1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Baconimport fs from 'fs'; 3082815dcSEvan Baconimport { vol } from 'memfs'; 4082815dcSEvan Baconimport path from 'path'; 5082815dcSEvan Bacon 6ed3bd27bSEvan Baconimport rnFixture from '../../plugins/__tests__/fixtures/react-native-project'; 7166385e2SDouglas Lowderimport { format } from '../../utils/XML'; 8ed3bd27bSEvan Baconimport * as XML from '../../utils/XML'; 9ed3bd27bSEvan Baconimport { AndroidManifest, getMainApplication } from '../Manifest'; 10166385e2SDouglas Lowderimport { readResourcesXMLAsync } from '../Resources'; 11082815dcSEvan Baconimport * as Updates from '../Updates'; 12082815dcSEvan Bacon 13ed3bd27bSEvan Baconasync function getFixtureManifestAsync() { 14ed3bd27bSEvan Bacon return (await XML.parseXMLAsync( 15ed3bd27bSEvan Bacon rnFixture['android/app/src/main/AndroidManifest.xml'] 16ed3bd27bSEvan Bacon )) as AndroidManifest; 17ed3bd27bSEvan Bacon} 18ed3bd27bSEvan Bacon 19082815dcSEvan Baconconst fixturesPath = path.resolve(__dirname, 'fixtures'); 20082815dcSEvan Baconconst sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem'); 21082815dcSEvan Bacon 22082815dcSEvan Baconjest.mock('fs'); 23082815dcSEvan Baconjest.mock('resolve-from'); 24082815dcSEvan Bacon 25082815dcSEvan Baconconst { silent } = require('resolve-from'); 26082815dcSEvan Bacon 27082815dcSEvan Baconconst fsReal = jest.requireActual('fs') as typeof import('fs'); 28082815dcSEvan Bacon 29082815dcSEvan Bacondescribe('Android Updates config', () => { 30082815dcSEvan Bacon beforeEach(() => { 31082815dcSEvan Bacon const resolveFrom = require('resolve-from'); 32082815dcSEvan Bacon resolveFrom.silent = silent; 33082815dcSEvan Bacon vol.reset(); 34082815dcSEvan Bacon }); 35082815dcSEvan Bacon 36082815dcSEvan Bacon it('set correct values in AndroidManifest.xml', async () => { 37082815dcSEvan Bacon vol.fromJSON({ 38082815dcSEvan Bacon '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'), 39082815dcSEvan Bacon }); 40082815dcSEvan Bacon 41ed3bd27bSEvan Bacon let androidManifestJson = await getFixtureManifestAsync(); 42082815dcSEvan Bacon const config: ExpoConfig = { 43082815dcSEvan Bacon name: 'foo', 44082815dcSEvan Bacon sdkVersion: '37.0.0', 45082815dcSEvan Bacon slug: 'my-app', 46082815dcSEvan Bacon owner: 'owner', 47082815dcSEvan Bacon updates: { 48082815dcSEvan Bacon enabled: false, 49082815dcSEvan Bacon fallbackToCacheTimeout: 2000, 50082815dcSEvan Bacon checkAutomatically: 'ON_ERROR_RECOVERY', 51082815dcSEvan Bacon codeSigningCertificate: 'hello', 52082815dcSEvan Bacon codeSigningMetadata: { 53082815dcSEvan Bacon alg: 'rsa-v1_5-sha256', 54082815dcSEvan Bacon keyid: 'test', 55082815dcSEvan Bacon }, 5609bd1012SUmberto Ghio requestHeaders: { 5709bd1012SUmberto Ghio 'expo-channel-name': 'test', 58fc285a6fSBartosz Kaszubowski testheader: 'test', 59fc285a6fSBartosz Kaszubowski }, 60082815dcSEvan Bacon }, 61082815dcSEvan Bacon }; 62*f0d67e12SMateus Craveiro androidManifestJson = await Updates.setUpdatesConfigAsync( 63*f0d67e12SMateus Craveiro '/app', 64*f0d67e12SMateus Craveiro config, 65*f0d67e12SMateus Craveiro androidManifestJson, 66*f0d67e12SMateus Craveiro '0.11.0' 67*f0d67e12SMateus Craveiro ); 68ed3bd27bSEvan Bacon const mainApplication = getMainApplication(androidManifestJson)!; 69ed3bd27bSEvan Bacon 70ed3bd27bSEvan Bacon if (!mainApplication['meta-data']) { 71ed3bd27bSEvan Bacon throw new Error('No meta-data found in AndroidManifest.xml'); 72ed3bd27bSEvan Bacon } 73082815dcSEvan Bacon 74082815dcSEvan Bacon const updateUrl = mainApplication['meta-data'].filter( 75082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL' 76082815dcSEvan Bacon ); 772fae8288SWill Schurman expect(updateUrl).toHaveLength(0); 78082815dcSEvan Bacon 79082815dcSEvan Bacon const sdkVersion = mainApplication['meta-data'].filter( 80082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION' 81082815dcSEvan Bacon ); 82082815dcSEvan Bacon expect(sdkVersion).toHaveLength(1); 83082815dcSEvan Bacon expect(sdkVersion[0].$['android:value']).toMatch('37.0.0'); 84082815dcSEvan Bacon 85082815dcSEvan Bacon const enabled = mainApplication['meta-data'].filter( 86082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.ENABLED' 87082815dcSEvan Bacon ); 88082815dcSEvan Bacon expect(enabled).toHaveLength(1); 89082815dcSEvan Bacon expect(enabled[0].$['android:value']).toMatch('false'); 90082815dcSEvan Bacon 91082815dcSEvan Bacon const checkOnLaunch = mainApplication['meta-data'].filter( 92082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH' 93082815dcSEvan Bacon ); 94082815dcSEvan Bacon expect(checkOnLaunch).toHaveLength(1); 95082815dcSEvan Bacon expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY'); 96082815dcSEvan Bacon 97082815dcSEvan Bacon const timeout = mainApplication['meta-data'].filter( 98082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS' 99082815dcSEvan Bacon ); 100082815dcSEvan Bacon expect(timeout).toHaveLength(1); 101082815dcSEvan Bacon expect(timeout[0].$['android:value']).toMatch('2000'); 102082815dcSEvan Bacon 103082815dcSEvan Bacon const codeSigningCertificate = mainApplication['meta-data'].filter( 104082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE' 105082815dcSEvan Bacon ); 106082815dcSEvan Bacon expect(codeSigningCertificate).toHaveLength(1); 107082815dcSEvan Bacon expect(codeSigningCertificate[0].$['android:value']).toMatch( 108082815dcSEvan Bacon fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8') 109082815dcSEvan Bacon ); 110082815dcSEvan Bacon 111082815dcSEvan Bacon const codeSigningMetadata = mainApplication['meta-data'].filter( 112082815dcSEvan Bacon (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA' 113082815dcSEvan Bacon ); 114082815dcSEvan Bacon expect(codeSigningMetadata).toHaveLength(1); 115082815dcSEvan Bacon expect(codeSigningMetadata[0].$['android:value']).toMatch( 116082815dcSEvan Bacon '{"alg":"rsa-v1_5-sha256","keyid":"test"}' 117082815dcSEvan Bacon ); 118166385e2SDouglas Lowder 11909bd1012SUmberto Ghio const requestHeaders = mainApplication['meta-data'].filter( 120fc285a6fSBartosz Kaszubowski (e) => 121fc285a6fSBartosz Kaszubowski e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY' 12209bd1012SUmberto Ghio ); 12309bd1012SUmberto Ghio expect(requestHeaders).toHaveLength(1); 12409bd1012SUmberto Ghio expect(requestHeaders[0].$['android:value']).toMatch( 12509bd1012SUmberto Ghio '{"expo-channel-name":"test","testheader":"test"}' 12609bd1012SUmberto Ghio ); 12709bd1012SUmberto Ghio 128166385e2SDouglas Lowder // For this config, runtime version should not be defined, so check that it does not appear in the manifest 129166385e2SDouglas Lowder const runtimeVersion = mainApplication['meta-data']?.filter( 130166385e2SDouglas Lowder (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION' 131166385e2SDouglas Lowder ); 132166385e2SDouglas Lowder expect(runtimeVersion).toHaveLength(0); 133082815dcSEvan Bacon }); 134082815dcSEvan Bacon 135082815dcSEvan Bacon describe(Updates.ensureBuildGradleContainsConfigurationScript, () => { 136082815dcSEvan Bacon it('adds create-manifest-android.gradle line to build.gradle', async () => { 137082815dcSEvan Bacon vol.fromJSON( 138082815dcSEvan Bacon { 139082815dcSEvan Bacon 'android/app/build.gradle': fsReal.readFileSync( 140082815dcSEvan Bacon path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'), 141082815dcSEvan Bacon 'utf-8' 142082815dcSEvan Bacon ), 143082815dcSEvan Bacon 'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever', 144082815dcSEvan Bacon }, 145082815dcSEvan Bacon '/app' 146082815dcSEvan Bacon ); 147082815dcSEvan Bacon 148ed3bd27bSEvan Bacon const contents = rnFixture['android/app/build.gradle']; 149082815dcSEvan Bacon const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents); 150082815dcSEvan Bacon expect(newContents).toMatchSnapshot(); 151082815dcSEvan Bacon }); 152082815dcSEvan Bacon 153082815dcSEvan Bacon it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => { 154082815dcSEvan Bacon // Pseudo node module resolution since actually mocking it could prove challenging. 155082815dcSEvan Bacon // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked. 156082815dcSEvan Bacon const resolveFrom = require('resolve-from'); 157082815dcSEvan Bacon resolveFrom.silent = (p, a) => { 158082815dcSEvan Bacon return silent(path.join(p, '..'), a); 159082815dcSEvan Bacon }; 160082815dcSEvan Bacon 161082815dcSEvan Bacon vol.fromJSON( 162082815dcSEvan Bacon { 163082815dcSEvan Bacon 'workspace/android/app/build.gradle': fsReal.readFileSync( 164082815dcSEvan Bacon path.join( 165082815dcSEvan Bacon __dirname, 166082815dcSEvan Bacon 'fixtures/build-with-incorrect-create-manifest-android-path.gradle' 167082815dcSEvan Bacon ), 168082815dcSEvan Bacon 'utf-8' 169082815dcSEvan Bacon ), 170082815dcSEvan Bacon 'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever', 171082815dcSEvan Bacon }, 172082815dcSEvan Bacon '/app' 173082815dcSEvan Bacon ); 174082815dcSEvan Bacon 175082815dcSEvan Bacon const contents = await fs.promises.readFile( 176082815dcSEvan Bacon '/app/workspace/android/app/build.gradle', 177082815dcSEvan Bacon 'utf-8' 178082815dcSEvan Bacon ); 179082815dcSEvan Bacon const newContents = Updates.ensureBuildGradleContainsConfigurationScript( 180082815dcSEvan Bacon '/app/workspace', 181082815dcSEvan Bacon contents 182082815dcSEvan Bacon ); 183082815dcSEvan Bacon expect(newContents).toMatchSnapshot(); 184082815dcSEvan Bacon }); 185082815dcSEvan Bacon }); 186166385e2SDouglas Lowder 187166385e2SDouglas Lowder describe('Runtime version tests', () => { 188166385e2SDouglas Lowder const sampleStringsXML = ` 189166385e2SDouglas Lowder<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 190166385e2SDouglas Lowder<resources> 191166385e2SDouglas Lowder</resources>`; 192166385e2SDouglas Lowder 193166385e2SDouglas Lowder beforeAll(async () => { 194166385e2SDouglas Lowder const directoryJSON = { 195166385e2SDouglas Lowder './android/app/src/main/res/values/strings.xml': sampleStringsXML, 196166385e2SDouglas Lowder }; 197166385e2SDouglas Lowder vol.fromJSON(directoryJSON, '/app'); 198166385e2SDouglas Lowder }); 199166385e2SDouglas Lowder 200166385e2SDouglas Lowder it('Correct metadata written to Android manifest with appVersion policy', async () => { 201166385e2SDouglas Lowder vol.fromJSON({ 202166385e2SDouglas Lowder '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'), 203166385e2SDouglas Lowder }); 204166385e2SDouglas Lowder 205ed3bd27bSEvan Bacon let androidManifestJson = await getFixtureManifestAsync(); 206166385e2SDouglas Lowder const config: ExpoConfig = { 207166385e2SDouglas Lowder name: 'foo', 208166385e2SDouglas Lowder version: '37.0.0', 209166385e2SDouglas Lowder slug: 'my-app', 210166385e2SDouglas Lowder owner: 'owner', 211166385e2SDouglas Lowder runtimeVersion: { 212166385e2SDouglas Lowder policy: 'appVersion', 213166385e2SDouglas Lowder }, 214166385e2SDouglas Lowder }; 215*f0d67e12SMateus Craveiro androidManifestJson = await Updates.setUpdatesConfigAsync( 216*f0d67e12SMateus Craveiro '/app', 217*f0d67e12SMateus Craveiro config, 218*f0d67e12SMateus Craveiro androidManifestJson, 219*f0d67e12SMateus Craveiro '0.11.0' 220*f0d67e12SMateus Craveiro ); 221166385e2SDouglas Lowder const mainApplication = getMainApplication(androidManifestJson); 222166385e2SDouglas Lowder 223ed3bd27bSEvan Bacon const runtimeVersion = mainApplication!['meta-data']?.filter( 224166385e2SDouglas Lowder (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION' 225166385e2SDouglas Lowder ); 226166385e2SDouglas Lowder expect(runtimeVersion).toHaveLength(1); 227166385e2SDouglas Lowder expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch( 228166385e2SDouglas Lowder '@string/expo_runtime_version' 229166385e2SDouglas Lowder ); 230166385e2SDouglas Lowder }); 231166385e2SDouglas Lowder 232166385e2SDouglas Lowder it('Write and clear runtime version in strings resource', async () => { 233166385e2SDouglas Lowder const stringsPath = '/app/android/app/src/main/res/values/strings.xml'; 234166385e2SDouglas Lowder const stringsJSON = await readResourcesXMLAsync({ path: stringsPath }); 235166385e2SDouglas Lowder const config = { 236166385e2SDouglas Lowder runtimeVersion: '1.10', 237*f0d67e12SMateus Craveiro modRequest: { 238*f0d67e12SMateus Craveiro projectRoot: '/', 239*f0d67e12SMateus Craveiro }, 240166385e2SDouglas Lowder }; 241*f0d67e12SMateus Craveiro await Updates.applyRuntimeVersionFromConfigAsync(config, stringsJSON); 242166385e2SDouglas Lowder expect(format(stringsJSON)).toEqual( 243166385e2SDouglas Lowder '<resources>\n <string name="expo_runtime_version">1.10</string>\n</resources>' 244166385e2SDouglas Lowder ); 245166385e2SDouglas Lowder 246166385e2SDouglas Lowder const config2 = { 247166385e2SDouglas Lowder sdkVersion: '1.10', 248*f0d67e12SMateus Craveiro modRequest: { 249*f0d67e12SMateus Craveiro projectRoot: '/', 250*f0d67e12SMateus Craveiro }, 251166385e2SDouglas Lowder }; 252*f0d67e12SMateus Craveiro await Updates.applyRuntimeVersionFromConfigAsync(config2, stringsJSON); 253166385e2SDouglas Lowder expect(format(stringsJSON)).toEqual('<resources/>'); 254166385e2SDouglas Lowder }); 255166385e2SDouglas Lowder 256166385e2SDouglas Lowder afterAll(async () => { 257166385e2SDouglas Lowder vol.reset(); 258166385e2SDouglas Lowder }); 259166385e2SDouglas Lowder }); 260082815dcSEvan Bacon}); 261