1import { ExpoConfig } from '@expo/config-types'; 2import fs from 'fs'; 3import { vol } from 'memfs'; 4import path from 'path'; 5 6import { format } from '../../utils/XML'; 7import { getMainApplication, readAndroidManifestAsync } from '../Manifest'; 8import { readResourcesXMLAsync } from '../Resources'; 9import * as Updates from '../Updates'; 10 11const fixturesPath = path.resolve(__dirname, 'fixtures'); 12const sampleManifestPath = path.resolve(fixturesPath, 'react-native-AndroidManifest.xml'); 13const sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem'); 14 15jest.mock('fs'); 16jest.mock('resolve-from'); 17 18const { silent } = require('resolve-from'); 19 20const fsReal = jest.requireActual('fs') as typeof import('fs'); 21 22describe('Android Updates config', () => { 23 beforeEach(() => { 24 const resolveFrom = require('resolve-from'); 25 resolveFrom.silent = silent; 26 vol.reset(); 27 }); 28 29 it('set correct values in AndroidManifest.xml', async () => { 30 vol.fromJSON({ 31 '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'), 32 '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'), 33 }); 34 35 let androidManifestJson = await readAndroidManifestAsync( 36 '/blah/react-native-AndroidManifest.xml' 37 ); 38 const config: ExpoConfig = { 39 name: 'foo', 40 sdkVersion: '37.0.0', 41 slug: 'my-app', 42 owner: 'owner', 43 updates: { 44 enabled: false, 45 fallbackToCacheTimeout: 2000, 46 checkAutomatically: 'ON_ERROR_RECOVERY', 47 codeSigningCertificate: 'hello', 48 codeSigningMetadata: { 49 alg: 'rsa-v1_5-sha256', 50 keyid: 'test', 51 }, 52 }, 53 }; 54 androidManifestJson = Updates.setUpdatesConfig( 55 '/app', 56 config, 57 androidManifestJson, 58 'user', 59 '0.11.0' 60 ); 61 const mainApplication = getMainApplication(androidManifestJson); 62 63 const updateUrl = mainApplication['meta-data'].filter( 64 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL' 65 ); 66 expect(updateUrl).toHaveLength(1); 67 expect(updateUrl[0].$['android:value']).toMatch('https://exp.host/@owner/my-app'); 68 69 const sdkVersion = mainApplication['meta-data'].filter( 70 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION' 71 ); 72 expect(sdkVersion).toHaveLength(1); 73 expect(sdkVersion[0].$['android:value']).toMatch('37.0.0'); 74 75 const enabled = mainApplication['meta-data'].filter( 76 (e) => e.$['android:name'] === 'expo.modules.updates.ENABLED' 77 ); 78 expect(enabled).toHaveLength(1); 79 expect(enabled[0].$['android:value']).toMatch('false'); 80 81 const checkOnLaunch = mainApplication['meta-data'].filter( 82 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH' 83 ); 84 expect(checkOnLaunch).toHaveLength(1); 85 expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY'); 86 87 const timeout = mainApplication['meta-data'].filter( 88 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS' 89 ); 90 expect(timeout).toHaveLength(1); 91 expect(timeout[0].$['android:value']).toMatch('2000'); 92 93 const codeSigningCertificate = mainApplication['meta-data'].filter( 94 (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE' 95 ); 96 expect(codeSigningCertificate).toHaveLength(1); 97 expect(codeSigningCertificate[0].$['android:value']).toMatch( 98 fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8') 99 ); 100 101 const codeSigningMetadata = mainApplication['meta-data'].filter( 102 (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA' 103 ); 104 expect(codeSigningMetadata).toHaveLength(1); 105 expect(codeSigningMetadata[0].$['android:value']).toMatch( 106 '{"alg":"rsa-v1_5-sha256","keyid":"test"}' 107 ); 108 109 // For this config, runtime version should not be defined, so check that it does not appear in the manifest 110 const runtimeVersion = mainApplication['meta-data']?.filter( 111 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION' 112 ); 113 expect(runtimeVersion).toHaveLength(0); 114 }); 115 116 describe(Updates.ensureBuildGradleContainsConfigurationScript, () => { 117 it('adds create-manifest-android.gradle line to build.gradle', async () => { 118 vol.fromJSON( 119 { 120 'android/app/build.gradle': fsReal.readFileSync( 121 path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'), 122 'utf-8' 123 ), 124 'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever', 125 }, 126 '/app' 127 ); 128 129 const contents = await fs.promises.readFile('/app/android/app/build.gradle', 'utf-8'); 130 const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents); 131 expect(newContents).toMatchSnapshot(); 132 }); 133 134 it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => { 135 // Pseudo node module resolution since actually mocking it could prove challenging. 136 // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked. 137 const resolveFrom = require('resolve-from'); 138 resolveFrom.silent = (p, a) => { 139 return silent(path.join(p, '..'), a); 140 }; 141 142 vol.fromJSON( 143 { 144 'workspace/android/app/build.gradle': fsReal.readFileSync( 145 path.join( 146 __dirname, 147 'fixtures/build-with-incorrect-create-manifest-android-path.gradle' 148 ), 149 'utf-8' 150 ), 151 'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever', 152 }, 153 '/app' 154 ); 155 156 const contents = await fs.promises.readFile( 157 '/app/workspace/android/app/build.gradle', 158 'utf-8' 159 ); 160 const newContents = Updates.ensureBuildGradleContainsConfigurationScript( 161 '/app/workspace', 162 contents 163 ); 164 expect(newContents).toMatchSnapshot(); 165 }); 166 }); 167 168 describe('Runtime version tests', () => { 169 const sampleStringsXML = ` 170<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 171<resources> 172</resources>`; 173 174 beforeAll(async () => { 175 const directoryJSON = { 176 './android/app/src/main/res/values/strings.xml': sampleStringsXML, 177 }; 178 vol.fromJSON(directoryJSON, '/app'); 179 }); 180 181 it('Correct metadata written to Android manifest with appVersion policy', async () => { 182 vol.fromJSON({ 183 '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'), 184 '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'), 185 }); 186 187 let androidManifestJson = await readAndroidManifestAsync( 188 '/blah/react-native-AndroidManifest.xml' 189 ); 190 const config: ExpoConfig = { 191 name: 'foo', 192 version: '37.0.0', 193 slug: 'my-app', 194 owner: 'owner', 195 runtimeVersion: { 196 policy: 'appVersion', 197 }, 198 }; 199 androidManifestJson = Updates.setUpdatesConfig( 200 '/app', 201 config, 202 androidManifestJson, 203 'user', 204 '0.11.0' 205 ); 206 const mainApplication = getMainApplication(androidManifestJson); 207 208 const runtimeVersion = mainApplication['meta-data']?.filter( 209 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION' 210 ); 211 expect(runtimeVersion).toHaveLength(1); 212 expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch( 213 '@string/expo_runtime_version' 214 ); 215 }); 216 217 it('Write and clear runtime version in strings resource', async () => { 218 const stringsPath = '/app/android/app/src/main/res/values/strings.xml'; 219 const stringsJSON = await readResourcesXMLAsync({ path: stringsPath }); 220 const config = { 221 runtimeVersion: '1.10', 222 }; 223 Updates.applyRuntimeVersionFromConfig(config, stringsJSON); 224 expect(format(stringsJSON)).toEqual( 225 '<resources>\n <string name="expo_runtime_version">1.10</string>\n</resources>' 226 ); 227 228 const config2 = { 229 sdkVersion: '1.10', 230 }; 231 Updates.applyRuntimeVersionFromConfig(config2, stringsJSON); 232 expect(format(stringsJSON)).toEqual('<resources/>'); 233 }); 234 235 afterAll(async () => { 236 vol.reset(); 237 }); 238 }); 239}); 240