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