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