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