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) => 115 e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY' 116 ); 117 expect(requestHeaders).toHaveLength(1); 118 expect(requestHeaders[0].$['android:value']).toMatch( 119 '{"expo-channel-name":"test","testheader":"test"}' 120 ); 121 122 // For this config, runtime version should not be defined, so check that it does not appear in the manifest 123 const runtimeVersion = mainApplication['meta-data']?.filter( 124 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION' 125 ); 126 expect(runtimeVersion).toHaveLength(0); 127 }); 128 129 describe(Updates.ensureBuildGradleContainsConfigurationScript, () => { 130 it('adds create-manifest-android.gradle line to build.gradle', async () => { 131 vol.fromJSON( 132 { 133 'android/app/build.gradle': fsReal.readFileSync( 134 path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'), 135 'utf-8' 136 ), 137 'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever', 138 }, 139 '/app' 140 ); 141 142 const contents = await fs.promises.readFile('/app/android/app/build.gradle', 'utf-8'); 143 const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents); 144 expect(newContents).toMatchSnapshot(); 145 }); 146 147 it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => { 148 // Pseudo node module resolution since actually mocking it could prove challenging. 149 // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked. 150 const resolveFrom = require('resolve-from'); 151 resolveFrom.silent = (p, a) => { 152 return silent(path.join(p, '..'), a); 153 }; 154 155 vol.fromJSON( 156 { 157 'workspace/android/app/build.gradle': fsReal.readFileSync( 158 path.join( 159 __dirname, 160 'fixtures/build-with-incorrect-create-manifest-android-path.gradle' 161 ), 162 'utf-8' 163 ), 164 'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever', 165 }, 166 '/app' 167 ); 168 169 const contents = await fs.promises.readFile( 170 '/app/workspace/android/app/build.gradle', 171 'utf-8' 172 ); 173 const newContents = Updates.ensureBuildGradleContainsConfigurationScript( 174 '/app/workspace', 175 contents 176 ); 177 expect(newContents).toMatchSnapshot(); 178 }); 179 }); 180 181 describe('Runtime version tests', () => { 182 const sampleStringsXML = ` 183<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 184<resources> 185</resources>`; 186 187 beforeAll(async () => { 188 const directoryJSON = { 189 './android/app/src/main/res/values/strings.xml': sampleStringsXML, 190 }; 191 vol.fromJSON(directoryJSON, '/app'); 192 }); 193 194 it('Correct metadata written to Android manifest with appVersion policy', async () => { 195 vol.fromJSON({ 196 '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'), 197 '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'), 198 }); 199 200 let androidManifestJson = await readAndroidManifestAsync( 201 '/blah/react-native-AndroidManifest.xml' 202 ); 203 const config: ExpoConfig = { 204 name: 'foo', 205 version: '37.0.0', 206 slug: 'my-app', 207 owner: 'owner', 208 runtimeVersion: { 209 policy: 'appVersion', 210 }, 211 }; 212 androidManifestJson = Updates.setUpdatesConfig( 213 '/app', 214 config, 215 androidManifestJson, 216 'user', 217 '0.11.0' 218 ); 219 const mainApplication = getMainApplication(androidManifestJson); 220 221 const runtimeVersion = mainApplication['meta-data']?.filter( 222 (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION' 223 ); 224 expect(runtimeVersion).toHaveLength(1); 225 expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch( 226 '@string/expo_runtime_version' 227 ); 228 }); 229 230 it('Write and clear runtime version in strings resource', async () => { 231 const stringsPath = '/app/android/app/src/main/res/values/strings.xml'; 232 const stringsJSON = await readResourcesXMLAsync({ path: stringsPath }); 233 const config = { 234 runtimeVersion: '1.10', 235 }; 236 Updates.applyRuntimeVersionFromConfig(config, stringsJSON); 237 expect(format(stringsJSON)).toEqual( 238 '<resources>\n <string name="expo_runtime_version">1.10</string>\n</resources>' 239 ); 240 241 const config2 = { 242 sdkVersion: '1.10', 243 }; 244 Updates.applyRuntimeVersionFromConfig(config2, stringsJSON); 245 expect(format(stringsJSON)).toEqual('<resources/>'); 246 }); 247 248 afterAll(async () => { 249 vol.reset(); 250 }); 251 }); 252}); 253