|
@@ -9,7 +9,7 @@ import * as psList from 'ps-list';
|
|
|
import { AddressInfo } from 'node:net';
|
|
|
import { ifdescribe, ifit } from './lib/spec-helpers';
|
|
|
import * as uuid from 'uuid';
|
|
|
-import { systemPreferences } from 'electron';
|
|
|
+import { autoUpdater, systemPreferences } from 'electron';
|
|
|
|
|
|
const features = process._linkedBinding('electron_common_features');
|
|
|
|
|
@@ -129,11 +129,13 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
|
|
|
|
|
const cachedZips: Record<string, string> = {};
|
|
|
|
|
|
- const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPostSign?: {
|
|
|
+ type Mutation = {
|
|
|
mutate: (appPath: string) => Promise<void>,
|
|
|
mutationKey: string,
|
|
|
- }) => {
|
|
|
- const key = `${version}-${fixture}-${mutateAppPostSign?.mutationKey || 'no-mutation'}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPreSign?: Mutation, mutateAppPostSign?: Mutation) => {
|
|
|
+ const key = `${version}-${fixture}-${mutateAppPreSign?.mutationKey || 'no-pre-mutation'}-${mutateAppPostSign?.mutationKey || 'no-post-mutation'}`;
|
|
|
if (!cachedZips[key]) {
|
|
|
let updateZipPath: string;
|
|
|
await withTempDirectory(async (dir) => {
|
|
@@ -143,6 +145,12 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
|
|
appPJPath,
|
|
|
(await fs.readFile(appPJPath, 'utf8')).replace('1.0.0', version)
|
|
|
);
|
|
|
+ const infoPath = path.resolve(secondAppPath, 'Contents', 'Info.plist');
|
|
|
+ await fs.writeFile(
|
|
|
+ infoPath,
|
|
|
+ (await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, `$1${version}`)
|
|
|
+ );
|
|
|
+ await mutateAppPreSign?.mutate(secondAppPath);
|
|
|
await signApp(secondAppPath);
|
|
|
await mutateAppPostSign?.mutate(secondAppPath);
|
|
|
updateZipPath = path.resolve(dir, 'update.zip');
|
|
@@ -279,16 +287,20 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
|
|
nextVersion: string;
|
|
|
startFixture: string;
|
|
|
endFixture: string;
|
|
|
- mutateAppPostSign?: {
|
|
|
- mutate: (appPath: string) => Promise<void>,
|
|
|
- mutationKey: string,
|
|
|
- }
|
|
|
+ mutateAppPreSign?: Mutation;
|
|
|
+ mutateAppPostSign?: Mutation;
|
|
|
}, fn: (appPath: string, zipPath: string) => Promise<void>) => {
|
|
|
await withTempDirectory(async (dir) => {
|
|
|
const appPath = await copyApp(dir, opts.startFixture);
|
|
|
+ await opts.mutateAppPreSign?.mutate(appPath);
|
|
|
+ const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
|
|
|
+ await fs.writeFile(
|
|
|
+ infoPath,
|
|
|
+ (await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, '$11.0.0')
|
|
|
+ );
|
|
|
await signApp(appPath);
|
|
|
|
|
|
- const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPostSign);
|
|
|
+ const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPreSign, opts.mutateAppPostSign);
|
|
|
|
|
|
await fn(appPath, updateZipPath);
|
|
|
});
|
|
@@ -335,6 +347,231 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+ 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 () => {
|
|
|
+ await withUpdatableApp({
|
|
|
+ nextVersion: '2.0.0',
|
|
|
+ startFixture: 'update-stack',
|
|
|
+ endFixture: 'update-stack'
|
|
|
+ }, async (appPath, updateZipPath2) => {
|
|
|
+ await withUpdatableApp({
|
|
|
+ nextVersion: '3.0.0',
|
|
|
+ startFixture: 'update-stack',
|
|
|
+ endFixture: 'update-stack'
|
|
|
+ }, async (_, updateZipPath3) => {
|
|
|
+ let updateCount = 0;
|
|
|
+ server.get('/update-file', (req, res) => {
|
|
|
+ res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
|
|
|
+ });
|
|
|
+ server.get('/update-check', (req, res) => {
|
|
|
+ updateCount++;
|
|
|
+ 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(4);
|
|
|
+ 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/');
|
|
|
+ expect(requests[2]).to.have.property('url', '/update-check');
|
|
|
+ expect(requests[3]).to.have.property('url', '/update-file');
|
|
|
+ expect(requests[2].header('user-agent')).to.include('Electron/');
|
|
|
+ expect(requests[3].header('user-agent')).to.include('Electron/');
|
|
|
+ });
|
|
|
+
|
|
|
+ await relaunchPromise;
|
|
|
+ expect(requests).to.have.lengthOf(5);
|
|
|
+ expect(requests[4].url).to.equal('/update-check/updated/3.0.0');
|
|
|
+ expect(requests[4].header('user-agent')).to.include('Electron/');
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should update to lower version numbers', async () => {
|
|
|
+ await withUpdatableApp({
|
|
|
+ nextVersion: '0.0.1',
|
|
|
+ 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/0.0.1');
|
|
|
+ expect(requests[2].header('user-agent')).to.include('Electron/');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('with ElectronSquirrelPreventDowngrades enabled', () => {
|
|
|
+ it('should not update to lower version numbers', async () => {
|
|
|
+ await withUpdatableApp({
|
|
|
+ nextVersion: '0.0.1',
|
|
|
+ startFixture: 'update',
|
|
|
+ endFixture: 'update',
|
|
|
+ mutateAppPreSign: {
|
|
|
+ mutationKey: 'prevent-downgrades',
|
|
|
+ mutate: async (appPath) => {
|
|
|
+ const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
|
|
|
+ await fs.writeFile(
|
|
|
+ infoPath,
|
|
|
+ (await fs.readFile(infoPath, 'utf8')).replace('<key>NSSupportsAutomaticGraphicsSwitching</key>', '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>')
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 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('Cannot update to a bundle with a lower version number');
|
|
|
+ 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 not update to version strings that are not simple Major.Minor.Patch', async () => {
|
|
|
+ await withUpdatableApp({
|
|
|
+ nextVersion: '2.0.0-bad',
|
|
|
+ startFixture: 'update',
|
|
|
+ endFixture: 'update',
|
|
|
+ mutateAppPreSign: {
|
|
|
+ mutationKey: 'prevent-downgrades',
|
|
|
+ mutate: async (appPath) => {
|
|
|
+ const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
|
|
|
+ await fs.writeFile(
|
|
|
+ infoPath,
|
|
|
+ (await fs.readFile(infoPath, 'utf8')).replace('<key>NSSupportsAutomaticGraphicsSwitching</key>', '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>')
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 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('Cannot update to a bundle with a lower version number');
|
|
|
+ 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 still update to higher version numbers', async () => {
|
|
|
+ await withUpdatableApp({
|
|
|
+ nextVersion: '1.0.1',
|
|
|
+ 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/1.0.1');
|
|
|
+ expect(requests[2].header('user-agent')).to.include('Electron/');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('should compare version numbers correctly', () => {
|
|
|
+ expect(autoUpdater.isVersionAllowedForUpdate('1.0.0', '2.0.0')).to.equal(true);
|
|
|
+ expect(autoUpdater.isVersionAllowedForUpdate('1.0.1', '1.0.10')).to.equal(true);
|
|
|
+ expect(autoUpdater.isVersionAllowedForUpdate('1.0.10', '1.0.1')).to.equal(false);
|
|
|
+ expect(autoUpdater.isVersionAllowedForUpdate('1.31.1', '1.32.0')).to.equal(true);
|
|
|
+ expect(autoUpdater.isVersionAllowedForUpdate('1.31.1', '0.32.0')).to.equal(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
it('should abort the update if the application is still running when ShipIt kicks off', async () => {
|
|
|
await withUpdatableApp({
|
|
|
nextVersion: '2.0.0',
|