2020-03-20 20:28:31 +00:00
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' ;
2019-03-30 00:32:52 +00:00
2020-06-23 03:32:45 +00:00
const features = process . _linkedBinding ( 'electron_common_features' ) ;
2019-03-30 00:32:52 +00:00
2020-03-20 20:28:31 +00:00
const fixturesPath = path . resolve ( __dirname , 'fixtures' ) ;
2019-03-30 00:32:52 +00:00
// We can only test the auto updater on darwin non-component builds
2020-03-20 20:28:31 +00:00
const describeFn = ( process . platform === 'darwin' && ! process . mas && ! features . isComponentBuild ( ) ? describe : describe.skip ) ;
2019-03-30 00:32:52 +00:00
describeFn ( '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 ( ) {
2020-03-20 20:28:31 +00:00
const result = cp . spawnSync ( path . resolve ( __dirname , '../script/codesign/get-trusted-identity.sh' ) ) ;
2019-11-01 20:37:02 +00:00
if ( result . status !== 0 || result . stdout . toString ( ) . trim ( ) . length === 0 ) {
2019-07-24 17:55:16 +00:00
// Per https://circleci.com/docs/2.0/env-vars:
// CIRCLE_PR_NUMBER is only present on forked PRs
2019-10-30 23:38:21 +00:00
if ( process . env . CI && ! process . env . CIRCLE_PR_NUMBER ) {
2020-03-20 20:28:31 +00:00
throw new Error ( 'No valid signing identity available to run autoUpdater specs' ) ;
2019-03-30 00:32:52 +00:00
}
2020-03-20 20:28:31 +00:00
this . skip ( ) ;
2019-03-30 00:32:52 +00:00
} else {
2020-03-20 20:28:31 +00:00
identity = result . stdout . toString ( ) . trim ( ) ;
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 copyApp = async ( newDir : string , fixture = 'initial' ) = > {
2020-03-20 20:28:31 +00:00
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' ) ;
2019-03-30 00:32:52 +00:00
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 > ` )
2020-03-20 20:28:31 +00:00
) ;
return newPath ;
} ;
2019-03-30 00:32:52 +00:00
const spawn = ( cmd : string , args : string [ ] , opts : any = { } ) = > {
2020-03-20 20:28:31 +00:00
let out = '' ;
const child = cp . spawn ( cmd , args , opts ) ;
2019-03-30 00:32:52 +00:00
child . stdout . on ( 'data' , ( chunk : Buffer ) = > {
2020-03-20 20:28:31 +00:00
out += chunk . toString ( ) ;
} ) ;
2019-03-30 00:32:52 +00:00
child . stderr . on ( 'data' , ( chunk : Buffer ) = > {
2020-03-20 20:28:31 +00:00
out += chunk . toString ( ) ;
} ) ;
2019-03-30 00:32:52 +00:00
return new Promise < { code : number , out : string } > ( ( resolve ) = > {
child . on ( 'exit' , ( code , signal ) = > {
2020-03-20 20:28:31 +00:00
expect ( signal ) . to . equal ( null ) ;
2019-03-30 00:32:52 +00:00
resolve ( {
code : code ! ,
out
2020-03-20 20:28:31 +00:00
} ) ;
} ) ;
} ) ;
} ;
2019-03-30 00:32:52 +00:00
const signApp = ( appPath : string ) = > {
2020-03-20 20:28:31 +00:00
return spawn ( 'codesign' , [ '-s' , identity , '--deep' , '--force' , appPath ] ) ;
} ;
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
2020-07-20 16:51:33 +00:00
const withTempDirectory = async ( fn : ( dir : string ) = > Promise < void > , autoCleanUp = true ) = > {
2020-03-20 20:28:31 +00:00
const dir = await fs . mkdtemp ( path . resolve ( os . tmpdir ( ) , 'electron-update-spec-' ) ) ;
2019-03-30 00:32:52 +00:00
try {
2020-03-20 20:28:31 +00:00
await fn ( dir ) ;
2019-03-30 00:32:52 +00:00
} finally {
2020-07-20 16:51:33 +00:00
if ( autoCleanUp ) {
cp . spawnSync ( 'rm' , [ '-r' , dir ] ) ;
}
2019-03-30 00:32:52 +00:00
}
2020-03-20 20:28:31 +00:00
} ;
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 > = { } ;
const getOrCreateUpdateZipPath = async ( version : string , fixture : string ) = > {
const key = ` ${ version } - ${ fixture } ` ;
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 ) ;
updateZipPath = path . resolve ( dir , 'update.zip' ) ;
await spawn ( 'zip' , [ '-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 ] ) ] ) ;
}
} ) ;
2019-03-30 00:32:52 +00:00
it ( 'should fail to set the feed URL when the app is not signed' , async ( ) = > {
await withTempDirectory ( async ( dir ) = > {
2020-03-20 20:28:31 +00:00
const appPath = await copyApp ( dir ) ;
const launchResult = await launchApp ( appPath , [ 'http://myupdate' ] ) ;
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 ) = > {
2020-03-20 20:28:31 +00:00
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' ) ;
} ) ;
} ) ;
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 ) {
2020-07-20 16:51:33 +00:00
await new Promise ( resolve = > {
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 ) = > {
2020-03-20 20:28:31 +00:00
const appPath = await copyApp ( dir , 'check' ) ;
await signApp ( appPath ) ;
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 ) = > {
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' ) ;
} ) ;
} ) ;
} ) ;
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 ) = > {
2020-03-20 20:28:31 +00:00
const appPath = await copyApp ( dir , 'update' ) ;
await signApp ( appPath ) ;
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 ;
} , fn : ( appPath : string , zipPath : string ) = > Promise < void > ) = > {
2019-03-30 00:32:52 +00:00
await withTempDirectory ( async ( dir ) = > {
2020-07-20 16:51:33 +00:00
const appPath = await copyApp ( dir , opts . startFixture ) ;
2020-03-20 20:28:31 +00:00
await signApp ( appPath ) ;
2019-03-30 00:32:52 +00:00
2020-07-20 16:51:33 +00:00
const updateZipPath = await getOrCreateUpdateZipPath ( opts . nextVersion , opts . endFixture ) ;
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
} ) ;
const relaunchPromise = new Promise ( ( resolve ) = > {
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 ) ;
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 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
} ) ;
const relaunchPromise = new Promise ( ( resolve ) = > {
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
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;