2020-03-20 20:28:31 +00:00
import { expect } from 'chai' ;
2023-06-15 14:42:27 +00:00
import * as cp from 'node:child_process' ;
import * as http from 'node:http' ;
2020-03-20 20:28:31 +00:00
import * as express from 'express' ;
2024-06-28 17:13:46 +00:00
import * as fs from 'node:fs' ;
2023-06-15 14:42:27 +00:00
import * as path from 'node:path' ;
2022-11-14 18:12:16 +00:00
import * as psList from 'ps-list' ;
2023-06-15 14:42:27 +00:00
import { AddressInfo } from 'node:net' ;
2023-01-25 21:01:25 +00:00
import { ifdescribe , ifit } from './lib/spec-helpers' ;
2024-05-13 20:36:03 +00:00
import { copyMacOSFixtureApp , getCodesignIdentity , shouldRunCodesignTests , signApp , spawn } from './lib/codesign-helpers' ;
2022-03-29 21:47:34 +00:00
import * as uuid from 'uuid' ;
2023-09-11 18:54:51 +00:00
import { autoUpdater , systemPreferences } from 'electron' ;
2024-05-13 20:36:03 +00:00
import { withTempDirectory } from './lib/fs-helpers' ;
2019-03-30 00:32:52 +00:00
// We can only test the auto updater on darwin non-component builds
2023-12-06 02:23:54 +00:00
ifdescribe ( shouldRunCodesignTests ) ( 'autoUpdater behavior' , function ( ) {
2020-03-20 20:28:31 +00:00
this . timeout ( 120000 ) ;
2019-03-30 00:32:52 +00:00
2020-03-20 20:28:31 +00:00
let identity = '' ;
2019-03-30 00:32:52 +00:00
beforeEach ( function ( ) {
2023-12-06 02:23:54 +00:00
const result = getCodesignIdentity ( ) ;
if ( result === null ) {
2020-03-20 20:28:31 +00:00
this . skip ( ) ;
2019-03-30 00:32:52 +00:00
} else {
2023-12-06 02:23:54 +00:00
identity = result ;
2019-03-30 00:32:52 +00:00
}
2020-03-20 20:28:31 +00:00
} ) ;
2019-03-30 00:32:52 +00:00
it ( 'should have a valid code signing identity' , ( ) = > {
2020-03-20 20:28:31 +00:00
expect ( identity ) . to . be . a ( 'string' ) . with . lengthOf . at . least ( 1 ) ;
} ) ;
2019-03-30 00:32:52 +00:00
const launchApp = ( appPath : string , args : string [ ] = [ ] ) = > {
2020-03-20 20:28:31 +00:00
return spawn ( path . resolve ( appPath , 'Contents/MacOS/Electron' ) , args ) ;
} ;
2019-03-30 00:32:52 +00:00
2022-11-14 18:12:16 +00:00
const spawnAppWithHandle = ( appPath : string , args : string [ ] = [ ] ) = > {
return cp . spawn ( path . resolve ( appPath , 'Contents/MacOS/Electron' ) , args ) ;
} ;
const getRunningShipIts = async ( appPath : string ) = > {
const processes = await psList ( ) ;
const activeShipIts = processes . filter ( p = > p . cmd ? . includes ( 'Squirrel.framework/Resources/ShipIt com.github.Electron.ShipIt' ) && p . cmd ! . startsWith ( appPath ) ) ;
return activeShipIts ;
} ;
2019-03-30 00:32:52 +00:00
const logOnError = ( what : any , fn : ( ) = > void ) = > {
try {
2020-03-20 20:28:31 +00:00
fn ( ) ;
2019-03-30 00:32:52 +00:00
} catch ( err ) {
2020-03-20 20:28:31 +00:00
console . error ( what ) ;
throw err ;
2019-03-30 00:32:52 +00:00
}
2020-03-20 20:28:31 +00:00
} ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
const cachedZips : Record < string , string > = { } ;
2023-09-11 18:54:51 +00:00
type Mutation = {
2022-03-22 07:19:46 +00:00
mutate : ( appPath : string ) = > Promise < void > ,
mutationKey : string ,
2023-09-11 18:54:51 +00:00
} ;
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' } ` ;
2020-07-20 16:51:33 +00:00
if ( ! cachedZips [ key ] ) {
let updateZipPath : string ;
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const secondAppPath = await copyMacOSFixtureApp ( dir , fixture ) ;
2020-07-20 16:51:33 +00:00
const appPJPath = path . resolve ( secondAppPath , 'Contents' , 'Resources' , 'app' , 'package.json' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . writeFile (
2020-07-20 16:51:33 +00:00
appPJPath ,
2024-06-28 17:13:46 +00:00
( await fs . promises . readFile ( appPJPath , 'utf8' ) ) . replace ( '1.0.0' , version )
2020-07-20 16:51:33 +00:00
) ;
2023-09-11 18:54:51 +00:00
const infoPath = path . resolve ( secondAppPath , 'Contents' , 'Info.plist' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . writeFile (
2023-09-11 18:54:51 +00:00
infoPath ,
2024-06-28 17:13:46 +00:00
( await fs . promises . readFile ( infoPath , 'utf8' ) ) . replace ( /(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g , ` $ 1 ${ version } ` )
2023-09-11 18:54:51 +00:00
) ;
await mutateAppPreSign ? . mutate ( secondAppPath ) ;
2023-12-06 02:23:54 +00:00
await signApp ( secondAppPath , identity ) ;
2022-03-22 07:19:46 +00:00
await mutateAppPostSign ? . mutate ( secondAppPath ) ;
2020-07-20 16:51:33 +00:00
updateZipPath = path . resolve ( dir , 'update.zip' ) ;
2022-03-29 21:47:34 +00:00
await spawn ( 'zip' , [ '-0' , '-r' , '--symlinks' , updateZipPath , './' ] , {
2020-07-20 16:51:33 +00:00
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 ] ) ] ) ;
}
} ) ;
2022-03-22 07:19:46 +00:00
// 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 ( ) = > {
2019-03-30 00:32:52 +00:00
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const appPath = await copyMacOSFixtureApp ( dir ) ;
2020-03-20 20:28:31 +00:00
const launchResult = await launchApp ( appPath , [ 'http://myupdate' ] ) ;
2022-03-22 07:19:46 +00:00
console . log ( launchResult ) ;
2020-03-20 20:28:31 +00:00
expect ( launchResult . code ) . to . equal ( 1 ) ;
expect ( launchResult . out ) . to . include ( 'Could not get code signature for running application' ) ;
} ) ;
} ) ;
2019-03-30 00:32:52 +00:00
it ( 'should cleanly set the feed URL when the app is signed' , async ( ) = > {
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const appPath = await copyMacOSFixtureApp ( dir ) ;
2023-12-06 02:23:54 +00:00
await signApp ( appPath , identity ) ;
2020-03-20 20:28:31 +00:00
const launchResult = await launchApp ( appPath , [ 'http://myupdate' ] ) ;
expect ( launchResult . code ) . to . equal ( 0 ) ;
expect ( launchResult . out ) . to . include ( 'Feed URL Set: http://myupdate' ) ;
} ) ;
} ) ;
2019-03-30 00:32:52 +00:00
describe ( 'with update server' , ( ) = > {
2020-03-20 20:28:31 +00:00
let port = 0 ;
let server : express.Application = null as any ;
let httpServer : http.Server = null as any ;
let requests : express.Request [ ] = [ ] ;
2019-03-30 00:32:52 +00:00
beforeEach ( ( done ) = > {
2020-03-20 20:28:31 +00:00
requests = [ ] ;
server = express ( ) ;
2019-03-30 00:32:52 +00:00
server . use ( ( req , res , next ) = > {
2020-03-20 20:28:31 +00:00
requests . push ( req ) ;
next ( ) ;
} ) ;
2019-03-30 00:32:52 +00:00
httpServer = server . listen ( 0 , '127.0.0.1' , ( ) = > {
2020-03-20 20:28:31 +00:00
port = ( httpServer . address ( ) as AddressInfo ) . port ;
done ( ) ;
} ) ;
} ) ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
afterEach ( async ( ) = > {
2019-03-30 00:32:52 +00:00
if ( httpServer ) {
2021-01-22 19:25:47 +00:00
await new Promise < void > ( resolve = > {
2020-07-20 16:51:33 +00:00
httpServer . close ( ( ) = > {
httpServer = null as any ;
server = null as any ;
resolve ( ) ;
} ) ;
2020-03-20 20:28:31 +00:00
} ) ;
2019-03-30 00:32:52 +00:00
}
2020-03-20 20:28:31 +00:00
} ) ;
2019-03-30 00:32:52 +00:00
it ( 'should hit the update endpoint when checkForUpdates is called' , async ( ) = > {
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const appPath = await copyMacOSFixtureApp ( dir , 'check' ) ;
2023-12-06 02:23:54 +00:00
await signApp ( appPath , identity ) ;
2019-03-30 00:32:52 +00:00
server . get ( '/update-check' , ( req , res ) = > {
2020-03-20 20:28:31 +00:00
res . status ( 204 ) . send ( ) ;
} ) ;
const launchResult = await launchApp ( appPath , [ ` http://localhost: ${ port } /update-check ` ] ) ;
2019-03-30 00:32:52 +00:00
logOnError ( launchResult , ( ) = > {
2020-03-20 20:28:31 +00:00
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/' ) ;
} ) ;
} ) ;
} ) ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
it ( 'should hit the update endpoint with customer headers when checkForUpdates is called' , async ( ) = > {
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const appPath = await copyMacOSFixtureApp ( dir , 'check-with-headers' ) ;
2023-12-06 02:23:54 +00:00
await signApp ( appPath , identity ) ;
2020-07-20 16:51:33 +00:00
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' ) ;
} ) ;
} ) ;
} ) ;
2019-03-30 00:32:52 +00:00
it ( 'should hit the download endpoint when an update is available and error if the file is bad' , async ( ) = > {
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const appPath = await copyMacOSFixtureApp ( dir , 'update' ) ;
2023-12-06 02:23:54 +00:00
await signApp ( appPath , identity ) ;
2019-03-30 00:32:52 +00:00
server . get ( '/update-file' , ( req , res ) = > {
2020-03-20 20:28:31 +00:00
res . status ( 500 ) . send ( 'This is not a file' ) ;
} ) ;
2019-03-30 00:32:52 +00:00
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 ( )
2020-03-20 20:28:31 +00:00
} ) ;
} ) ;
const launchResult = await launchApp ( appPath , [ ` http://localhost: ${ port } /update-check ` ] ) ;
2019-03-30 00:32:52 +00:00
logOnError ( launchResult , ( ) = > {
2020-03-20 20:28:31 +00:00
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/' ) ;
} ) ;
} ) ;
} ) ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
const withUpdatableApp = async ( opts : {
nextVersion : string ;
startFixture : string ;
endFixture : string ;
2023-09-11 18:54:51 +00:00
mutateAppPreSign? : Mutation ;
mutateAppPostSign? : Mutation ;
2020-07-20 16:51:33 +00:00
} , fn : ( appPath : string , zipPath : string ) = > Promise < void > ) = > {
2019-03-30 00:32:52 +00:00
await withTempDirectory ( async ( dir ) = > {
2024-05-13 20:36:03 +00:00
const appPath = await copyMacOSFixtureApp ( dir , opts . startFixture ) ;
2023-09-11 18:54:51 +00:00
await opts . mutateAppPreSign ? . mutate ( appPath ) ;
const infoPath = path . resolve ( appPath , 'Contents' , 'Info.plist' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . writeFile (
2023-09-11 18:54:51 +00:00
infoPath ,
2024-06-28 17:13:46 +00:00
( await fs . promises . readFile ( infoPath , 'utf8' ) ) . replace ( /(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g , '$11.0.0' )
2023-09-11 18:54:51 +00:00
) ;
2023-12-06 02:23:54 +00:00
await signApp ( appPath , identity ) ;
2019-03-30 00:32:52 +00:00
2023-09-11 18:54:51 +00:00
const updateZipPath = await getOrCreateUpdateZipPath ( opts . nextVersion , opts . endFixture , opts . mutateAppPreSign , opts . mutateAppPostSign ) ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
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 ( )
2020-03-20 20:28:31 +00:00
} ) ;
2020-07-20 16:51:33 +00:00
} ) ;
2021-01-22 19:25:47 +00:00
const relaunchPromise = new Promise < void > ( ( resolve ) = > {
2020-07-20 16:51:33 +00:00
server . get ( '/update-check/updated/:version' , ( req , res ) = > {
res . status ( 204 ) . send ( ) ;
resolve ( ) ;
2020-03-20 20:28:31 +00:00
} ) ;
2020-07-20 16:51:33 +00:00
} ) ;
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 ) ;
2020-10-13 02:01:49 +00:00
expect ( requests [ 2 ] . url ) . to . equal ( '/update-check/updated/2.0.0' ) ;
2020-07-20 16:51:33 +00:00
expect ( requests [ 2 ] . header ( 'user-agent' ) ) . to . include ( 'Electron/' ) ;
} ) ;
} ) ;
2023-09-11 18:54:51 +00:00
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' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . writeFile (
2023-09-11 18:54:51 +00:00
infoPath ,
2024-06-28 17:13:46 +00:00
( await fs . promises . readFile ( infoPath , 'utf8' ) ) . replace ( '<key>NSSupportsAutomaticGraphicsSwitching</key>' , '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>' )
2023-09-11 18:54:51 +00:00
) ;
}
}
} , 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' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . writeFile (
2023-09-11 18:54:51 +00:00
infoPath ,
2024-06-28 17:13:46 +00:00
( await fs . promises . readFile ( infoPath , 'utf8' ) ) . replace ( '<key>NSSupportsAutomaticGraphicsSwitching</key>' , '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>' )
2023-09-11 18:54:51 +00:00
) ;
}
}
} , 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' , ( ) = > {
2023-10-25 18:02:15 +00:00
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 ) ;
2023-09-11 18:54:51 +00:00
} ) ;
} ) ;
2022-11-14 18:12:16 +00:00
it ( 'should abort the update if the application is still running when ShipIt kicks off' , 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 ( )
} ) ;
} ) ;
enum FlipFlop {
INITIAL ,
FLIPPED ,
FLOPPED ,
}
const shipItFlipFlopPromise = new Promise < void > ( ( resolve ) = > {
let state = FlipFlop . INITIAL ;
const checker = setInterval ( async ( ) = > {
const running = await getRunningShipIts ( appPath ) ;
switch ( state ) {
case FlipFlop . INITIAL : {
if ( running . length ) state = FlipFlop . FLIPPED ;
break ;
}
case FlipFlop . FLIPPED : {
if ( ! running . length ) state = FlipFlop . FLOPPED ;
break ;
}
}
if ( state === FlipFlop . FLOPPED ) {
clearInterval ( checker ) ;
resolve ( ) ;
}
} , 500 ) ;
} ) ;
const launchResult = await launchApp ( appPath , [ ` http://localhost: ${ port } /update-check ` ] ) ;
const retainerHandle = spawnAppWithHandle ( appPath , [ 'remain-open' ] ) ;
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 shipItFlipFlopPromise ;
expect ( requests ) . to . have . lengthOf ( 2 , 'should not have relaunched the updated app' ) ;
2024-06-28 17:13:46 +00:00
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' ) ;
2022-11-14 18:12:16 +00:00
retainerHandle . kill ( 'SIGINT' ) ;
} ) ;
} ) ;
2022-03-29 21:47:34 +00:00
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 } ` ) ;
} ) ;
} ) ;
} ) ;
2022-03-22 07:19:46 +00:00
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' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . writeFile ( resourcesPath , 'demo' ) ;
2022-03-22 07:19:46 +00:00
}
}
} , 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' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . rm ( shipItPath , { force : true , recursive : true } ) ;
await fs . promises . symlink ( '/tmp/ShipIt' , shipItPath , 'file' ) ;
2022-03-22 07:19:46 +00:00
}
}
} , 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' ) ;
2024-06-28 17:13:46 +00:00
await fs . promises . appendFile ( shipItPath , Buffer . from ( '123' ) ) ;
2022-03-22 07:19:46 +00:00
}
}
} , 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/' ) ;
} ) ;
} ) ;
} ) ;
2020-07-20 16:51:33 +00:00
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 ( )
}
}
]
2020-03-20 20:28:31 +00:00
} ) ;
2020-07-20 16:51:33 +00:00
} ) ;
2021-01-22 19:25:47 +00:00
const relaunchPromise = new Promise < void > ( ( resolve ) = > {
2020-07-20 16:51:33 +00:00
server . get ( '/update-check/updated/:version' , ( req , res ) = > {
res . status ( 204 ) . send ( ) ;
resolve ( ) ;
2020-03-20 20:28:31 +00:00
} ) ;
2020-07-20 16:51:33 +00:00
} ) ;
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/' ) ;
} ) ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
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/' ) ;
2020-03-20 20:28:31 +00:00
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;