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