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