22
33import assert = require( "assert" ) ;
44import childProcess = require( "child_process" ) ;
5- import crypto = require( "crypto" ) ;
65import fs = require( "fs" ) ;
76import mkdirp = require( "mkdirp" ) ;
87import os = require( "os" ) ;
98import path = require( "path" ) ;
109import slash = require( "slash" ) ;
10+ import { promisify } from "util" ;
1111
1212import { Platform , PluginTestingFramework , ProjectManager , setupTestRunScenario , setupUpdateScenario , ServerUtil , TestBuilder , TestConfig , TestUtil } from "code-push-plugin-testing-framework" ;
1313
1414import Q = require( "q" ) ;
1515
1616import del = require( "del" ) ;
1717
18+ import { codeSigningPublicKey , signAndRecordUpdateArchive , setupTamperedSignatureUpdateScenario } from "./codesign" ;
19+
20+ // Used in test/template/app.json to avoid duplicating the PEM fixture in two places (ios and android plugin config).
21+ const CODE_SIGNING_PUBLIC_KEY_PLACEHOLDER = "{{CODE_SIGNING_PUBLIC_KEY}}" ;
22+
1823function ensureAndroidCleartextTraffic ( androidManifestPath : string ) : void {
1924 const androidManifestContents = fs . readFileSync ( androidManifestPath , "utf8" ) ;
2025
@@ -37,6 +42,10 @@ function ensureAndroidCleartextTraffic(androidManifestPath: string): void {
3742 }
3843}
3944
45+ async function setPlistStringValue ( plistPath : string , key : string , value : string ) : Promise < void > {
46+ await promisify ( childProcess . execFile ) ( "plutil" , [ "-replace" , key , "-string" , value , plistPath ] ) ;
47+ }
48+
4049/**
4150 * Returns a " --platform <ios|android>" flag for `expo prebuild` when exactly one platform is
4251 * under test in this mocha run, so prebuild only regenerates that platform's native project
@@ -69,47 +78,6 @@ function installExpoBundleTooling(projectPath: string): Q.Promise<void> {
6978 ) . then ( ( ) => { return null ; } ) ;
7079}
7180
72- const CODEPUSH_METADATA_FILE_NAME = ".codepushrelease" ;
73-
74- function isHashIgnored ( relativePath : string ) : boolean {
75- return relativePath . startsWith ( "__MACOSX/" )
76- || relativePath === ".DS_Store"
77- || relativePath . endsWith ( "/.DS_Store" )
78- || relativePath === CODEPUSH_METADATA_FILE_NAME
79- || relativePath . endsWith ( `/${ CODEPUSH_METADATA_FILE_NAME } ` ) ;
80- }
81-
82- /**
83- * Computes the same content hash that the native SDKs compute over an installed update folder, so the mock server
84- * can hand back a package_hash that will actually match what the client expects.
85- */
86- function computeUpdateContentsHash ( folderPath : string ) : string {
87- const manifest : string [ ] = [ ] ;
88-
89- const walk = ( currentPath : string , relativePrefix : string ) => {
90- for ( const entryName of fs . readdirSync ( currentPath ) ) {
91- const entryPath = path . join ( currentPath , entryName ) ;
92- const relativePath = relativePrefix ? `${ relativePrefix } /${ entryName } ` : entryName ;
93-
94- if ( isHashIgnored ( relativePath ) ) {
95- continue ;
96- }
97-
98- if ( fs . statSync ( entryPath ) . isDirectory ( ) ) {
99- walk ( entryPath , relativePath ) ;
100- } else {
101- const fileHash = crypto . createHash ( "sha256" ) . update ( fs . readFileSync ( entryPath ) ) . digest ( "hex" ) ;
102- manifest . push ( `${ relativePath } :${ fileHash } ` ) ;
103- }
104- }
105- } ;
106-
107- walk ( folderPath , "" ) ;
108- manifest . sort ( ) ;
109-
110- return crypto . createHash ( "sha256" ) . update ( JSON . stringify ( manifest ) ) . digest ( "hex" ) ;
111- }
112-
11381//////////////////////////////////////////////////////////////////////////////////////////
11482// Create the platforms to run the tests on.
11583
@@ -209,6 +177,7 @@ class RNAndroid extends Platform.Android implements RNPlatform {
209177 const string = path . join ( innerprojectDirectory , "android" , "app" , "src" , "main" , "res" , "values" , "strings.xml" ) ;
210178 TestUtil . replaceString ( string , TestUtil . SERVER_URL_PLACEHOLDER , this . getServerUrl ( ) ) ;
211179 TestUtil . replaceString ( string , TestUtil . ANDROID_KEY_PLACEHOLDER , this . getDefaultDeploymentKey ( ) ) ;
180+ TestUtil . replaceString ( string , "</resources>" , `<string moduleConfig="true" name="CodePushPublicKey">${ codeSigningPublicKey } </string>\n</resources>` ) ;
212181 TestUtil . replaceString ( AndroidManifest , "\\${usesCleartextTraffic}" , "true" ) ;
213182
214183
@@ -280,14 +249,10 @@ class RNIOS extends Platform.IOS implements RNPlatform {
280249 // Install the Podfile
281250 return TestUtil . copyFile ( path . join ( TestConfig . templatePath , "ios" , "Podfile" ) , podfilePath , true )
282251 . then ( ( ) => TestUtil . getProcessOutput ( `pod install` , { cwd : iOSProject , noLogStdOut : true } ) )
283- // Put the IOS deployment key in the Info.plist
284- . then ( TestUtil . replaceString . bind ( undefined , infoPlistPath ,
285- "</dict>\n</plist>" ,
286- "<key>CodePushDeploymentKey</key>\n\t<string>" + this . getDefaultDeploymentKey ( ) + "</string>\n\t<key>CodePushServerURL</key>\n\t<string>" + this . getServerUrl ( ) + "</string>\n\t</dict>\n</plist>" ) )
287- // Set the app version to 1.0.0 instead of 1.0 in the Info.plist
288- . then ( TestUtil . replaceString . bind ( undefined , infoPlistPath , "1.0" , "1.0.0" ) )
289- // Remove dependence of CFBundleShortVersionString from project.pbxproj
290- . then ( TestUtil . replaceString . bind ( undefined , infoPlistPath , "\\$\\(MARKETING_VERSION\\)" , "1.0.0" ) )
252+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CFBundleShortVersionString" , "1.0.0" ) )
253+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CodePushDeploymentKey" , this . getDefaultDeploymentKey ( ) ) )
254+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CodePushServerURL" , this . getServerUrl ( ) ) )
255+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CodePushPublicKey" , codeSigningPublicKey ) )
291256 // Fix the linker flag list in project.pbxproj (pod install adds an extra comma)
292257 . then ( TestUtil . replaceString . bind ( undefined , path . join ( iOSProject , TestConfig . TestAppName + ".xcodeproj" , "project.pbxproj" ) ,
293258 "\"[$][(]inherited[)]\",\\s*[)];" , "\"$(inherited)\"\n\t\t\t\t);" ) )
@@ -440,6 +405,12 @@ class RNProjectManager extends ProjectManager {
440405 return TestUtil . getProcessOutput ( `npx create-expo-app@latest ${ appName } --template blank@sdk-57` , { cwd : projectDirectory , timeout : 30 * 60 * 1000 , noLogStdOut : true } )
441406 . then ( ( e ) => { console . log ( `"npx expo init ${ appName } " success. cwd=${ projectDirectory } ` ) ; return e ; } )
442407 . then ( this . copyTemplate . bind ( this , templatePath , projectDirectory ) )
408+ . then ( ( ) => {
409+ const appJsonPath = path . join ( projectDirectory , TestConfig . TestAppName , "app.json" ) ;
410+ // app.json is JSON, so the PEM's line breaks must stay escaped rather than literal.
411+ const escapedPublicKey = codeSigningPublicKey . replace ( / \n / g, "\\n" ) ;
412+ TestUtil . replaceString ( appJsonPath , CODE_SIGNING_PUBLIC_KEY_PLACEHOLDER , escapedPublicKey ) ;
413+ } )
443414 . then < void > ( TestUtil . getProcessOutput . bind ( undefined , TestConfig . thisPluginInstallString , { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true , noLogStdErr : true } ) )
444415 . then ( installExpoBundleTooling . bind ( undefined , path . join ( projectDirectory , TestConfig . TestAppName ) ) )
445416 // create-expo-app's blank template ships without a metro.config.js. react-native-xcode.sh's
@@ -539,28 +510,19 @@ class RNProjectManager extends ProjectManager {
539510 . then ( TestUtil . getProcessOutput . bind ( undefined , "npx expo prebuild --platform " + targetPlatform . getName ( ) , { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true } ) )
540511 . then ( TestUtil . getProcessOutput . bind ( undefined , "npx react-native bundle --entry-file index.js --platform " + targetPlatform . getName ( ) + " --bundle-output " + bundlePath + " --assets-dest " + bundleFolder + " --dev false" ,
541512 { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true } ) )
513+ . then ( ( ) => signAndRecordUpdateArchive ( bundleFolder , isDiff ) )
542514 . then < string > ( TestUtil . archiveFolder . bind ( undefined , bundleFolder , "" , path . join ( projectDirectory , TestConfig . TestAppName , "update.zip" ) , isDiff ) )
543- . then < string > ( this . updateMockPackageHash . bind ( this , bundleFolder , isDiff ) )
544515 . then ( ( result ) => { console . log ( `[TIMING] createUpdateArchive(${ projectDirectory } , ${ targetPlatform . getName ( ) } ) took ${ Date . now ( ) - t0 } ms` ) ; return result ; } ) ;
545516 } else {
546517 return deferred . promise
547518 . then ( TestUtil . getProcessOutput . bind ( undefined , "npx react-native bundle --entry-file index.js --platform " + targetPlatform . getName ( ) + " --bundle-output " + bundlePath + " --assets-dest " + bundleFolder + " --dev false" ,
548519 { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true } ) )
520+ . then ( ( ) => signAndRecordUpdateArchive ( bundleFolder , isDiff ) )
549521 . then < string > ( TestUtil . archiveFolder . bind ( undefined , bundleFolder , "" , path . join ( projectDirectory , TestConfig . TestAppName , "update.zip" ) , isDiff ) )
550- . then < string > ( this . updateMockPackageHash . bind ( this , bundleFolder , isDiff ) )
551522 . then ( ( result ) => { console . log ( `[TIMING] createUpdateArchive(${ projectDirectory } , ${ targetPlatform . getName ( ) } ) took ${ Date . now ( ) - t0 } ms` ) ; return result ; } ) ;
552523 }
553524 }
554525
555- // Records the real hash of bundleFolder of an archive, so the mock server can hand back a
556- // package_hash that matches what the client's verifyFolderHash integrity check will compute.
557- private updateMockPackageHash ( bundleFolder : string , isDiff : boolean , archivePath : string ) : string {
558- // TODO(RA-4875): Diff updates clear it instead, since they are poorly implemented in the entire test harness.
559- // It's going to be a bigger refactor, so for now we just clear the known package hash to avoid using a stale value in diff tests.
560- ServerUtil . setKnownPackageHash ( isDiff ? undefined : computeUpdateContentsHash ( bundleFolder ) ) ;
561- return archivePath ;
562- }
563-
564526 /** JSON file containing the platforms the plugin is currently installed for.
565527 * Keys must match targetPlatform.getName()!
566528 *
@@ -1050,6 +1012,27 @@ PluginTestingFramework.initializeTests(new RNProjectManager(), supportedTargetPl
10501012 } ) ;
10511013 } , ScenarioInstall ) ;
10521014
1015+ TestBuilder . describe ( "#localPackage.install.codeSigning" ,
1016+ ( ) => {
1017+ TestBuilder . it ( "localPackage.install.codeSigning.tamperedSignature" , false ,
1018+ async ( done : Mocha . Done ) => {
1019+ try {
1020+ ServerUtil . updateResponse = { update_info : ServerUtil . createUpdateResponse ( false , targetPlatform ) } ;
1021+
1022+ /* create a normal update, then tamper with its signature after it's been signed */
1023+ const updatePath = await setupTamperedSignatureUpdateScenario ( projectManager , targetPlatform , UpdateNotifyApplicationReady , "Tampered Update" ) ;
1024+ ServerUtil . updatePackagePath = updatePath ;
1025+ projectManager . runApplication ( TestConfig . testRunDirectory , targetPlatform ) ;
1026+ await ServerUtil . expectTestMessages ( [
1027+ ServerUtil . TestMessage . CHECK_UPDATE_AVAILABLE ,
1028+ ServerUtil . TestMessage . DOWNLOAD_ERROR ] ) ;
1029+ done ( ) ;
1030+ } catch ( e ) {
1031+ done ( e ) ;
1032+ }
1033+ } ) ;
1034+ } , ScenarioInstall ) ;
1035+
10531036 TestBuilder . describe ( "#localPackage.install.revert" ,
10541037 ( ) => {
10551038 TestBuilder . it ( "localPackage.install.revert.dorevert" , false ,
0 commit comments