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