import { ExpoConfig } from '@expo/config-types';
import fs from 'fs';
import { vol } from 'memfs';
import path from 'path';
import rnFixture from '../../plugins/__tests__/fixtures/react-native-project';
import { format } from '../../utils/XML';
import * as XML from '../../utils/XML';
import { AndroidManifest, getMainApplication } from '../Manifest';
import { readResourcesXMLAsync } from '../Resources';
import * as Updates from '../Updates';
async function getFixtureManifestAsync() {
return (await XML.parseXMLAsync(
rnFixture['android/app/src/main/AndroidManifest.xml']
)) as AndroidManifest;
}
const fixturesPath = path.resolve(__dirname, 'fixtures');
const sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem');
jest.mock('fs');
jest.mock('resolve-from');
const { silent } = require('resolve-from');
const fsReal = jest.requireActual('fs') as typeof import('fs');
describe('Android Updates config', () => {
beforeEach(() => {
const resolveFrom = require('resolve-from');
resolveFrom.silent = silent;
vol.reset();
});
it('set correct values in AndroidManifest.xml', async () => {
vol.fromJSON({
'/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
});
let androidManifestJson = await getFixtureManifestAsync();
const config: ExpoConfig = {
name: 'foo',
sdkVersion: '37.0.0',
slug: 'my-app',
owner: 'owner',
updates: {
enabled: false,
fallbackToCacheTimeout: 2000,
checkAutomatically: 'ON_ERROR_RECOVERY',
codeSigningCertificate: 'hello',
codeSigningMetadata: {
alg: 'rsa-v1_5-sha256',
keyid: 'test',
},
requestHeaders: {
'expo-channel-name': 'test',
testheader: 'test',
},
},
};
androidManifestJson = await Updates.setUpdatesConfigAsync(
'/app',
config,
androidManifestJson,
'0.11.0'
);
const mainApplication = getMainApplication(androidManifestJson)!;
if (!mainApplication['meta-data']) {
throw new Error('No meta-data found in AndroidManifest.xml');
}
const updateUrl = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL'
);
expect(updateUrl).toHaveLength(0);
const sdkVersion = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION'
);
expect(sdkVersion).toHaveLength(1);
expect(sdkVersion[0].$['android:value']).toMatch('37.0.0');
const enabled = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.ENABLED'
);
expect(enabled).toHaveLength(1);
expect(enabled[0].$['android:value']).toMatch('false');
const checkOnLaunch = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH'
);
expect(checkOnLaunch).toHaveLength(1);
expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY');
const timeout = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS'
);
expect(timeout).toHaveLength(1);
expect(timeout[0].$['android:value']).toMatch('2000');
const codeSigningCertificate = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE'
);
expect(codeSigningCertificate).toHaveLength(1);
expect(codeSigningCertificate[0].$['android:value']).toMatch(
fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8')
);
const codeSigningMetadata = mainApplication['meta-data'].filter(
(e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA'
);
expect(codeSigningMetadata).toHaveLength(1);
expect(codeSigningMetadata[0].$['android:value']).toMatch(
'{"alg":"rsa-v1_5-sha256","keyid":"test"}'
);
const requestHeaders = mainApplication['meta-data'].filter(
(e) =>
e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY'
);
expect(requestHeaders).toHaveLength(1);
expect(requestHeaders[0].$['android:value']).toMatch(
'{"expo-channel-name":"test","testheader":"test"}'
);
// For this config, runtime version should not be defined, so check that it does not appear in the manifest
const runtimeVersion = mainApplication['meta-data']?.filter(
(e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
);
expect(runtimeVersion).toHaveLength(0);
});
describe(Updates.ensureBuildGradleContainsConfigurationScript, () => {
it('adds create-manifest-android.gradle line to build.gradle', async () => {
vol.fromJSON(
{
'android/app/build.gradle': fsReal.readFileSync(
path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'),
'utf-8'
),
'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
},
'/app'
);
const contents = rnFixture['android/app/build.gradle'];
const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents);
expect(newContents).toMatchSnapshot();
});
it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => {
// Pseudo node module resolution since actually mocking it could prove challenging.
// In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked.
const resolveFrom = require('resolve-from');
resolveFrom.silent = (p, a) => {
return silent(path.join(p, '..'), a);
};
vol.fromJSON(
{
'workspace/android/app/build.gradle': fsReal.readFileSync(
path.join(
__dirname,
'fixtures/build-with-incorrect-create-manifest-android-path.gradle'
),
'utf-8'
),
'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
},
'/app'
);
const contents = await fs.promises.readFile(
'/app/workspace/android/app/build.gradle',
'utf-8'
);
const newContents = Updates.ensureBuildGradleContainsConfigurationScript(
'/app/workspace',
contents
);
expect(newContents).toMatchSnapshot();
});
});
describe('Runtime version tests', () => {
const sampleStringsXML = `
`;
beforeAll(async () => {
const directoryJSON = {
'./android/app/src/main/res/values/strings.xml': sampleStringsXML,
};
vol.fromJSON(directoryJSON, '/app');
});
it('Correct metadata written to Android manifest with appVersion policy', async () => {
vol.fromJSON({
'/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
});
let androidManifestJson = await getFixtureManifestAsync();
const config: ExpoConfig = {
name: 'foo',
version: '37.0.0',
slug: 'my-app',
owner: 'owner',
runtimeVersion: {
policy: 'appVersion',
},
};
androidManifestJson = await Updates.setUpdatesConfigAsync(
'/app',
config,
androidManifestJson,
'0.11.0'
);
const mainApplication = getMainApplication(androidManifestJson);
const runtimeVersion = mainApplication!['meta-data']?.filter(
(e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
);
expect(runtimeVersion).toHaveLength(1);
expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch(
'@string/expo_runtime_version'
);
});
it('Write and clear runtime version in strings resource', async () => {
const stringsPath = '/app/android/app/src/main/res/values/strings.xml';
const stringsJSON = await readResourcesXMLAsync({ path: stringsPath });
const config = {
runtimeVersion: '1.10',
modRequest: {
projectRoot: '/',
},
};
await Updates.applyRuntimeVersionFromConfigAsync(config, stringsJSON);
expect(format(stringsJSON)).toEqual(
'\n 1.10\n'
);
const config2 = {
sdkVersion: '1.10',
modRequest: {
projectRoot: '/',
},
};
await Updates.applyRuntimeVersionFromConfigAsync(config2, stringsJSON);
expect(format(stringsJSON)).toEqual('');
});
afterAll(async () => {
vol.reset();
});
});
});