1import { ExpoConfig } from '@expo/config-types';
2import fs from 'fs';
3import { vol } from 'memfs';
4import path from 'path';
5
6import { format } from '../../utils/XML';
7import { getMainApplication, readAndroidManifestAsync } from '../Manifest';
8import { readResourcesXMLAsync } from '../Resources';
9import * as Updates from '../Updates';
10
11const fixturesPath = path.resolve(__dirname, 'fixtures');
12const sampleManifestPath = path.resolve(fixturesPath, 'react-native-AndroidManifest.xml');
13const sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem');
14
15jest.mock('fs');
16jest.mock('resolve-from');
17
18const { silent } = require('resolve-from');
19
20const fsReal = jest.requireActual('fs') as typeof import('fs');
21
22describe('Android Updates config', () => {
23  beforeEach(() => {
24    const resolveFrom = require('resolve-from');
25    resolveFrom.silent = silent;
26    vol.reset();
27  });
28
29  it('set correct values in AndroidManifest.xml', async () => {
30    vol.fromJSON({
31      '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'),
32      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
33    });
34
35    let androidManifestJson = await readAndroidManifestAsync(
36      '/blah/react-native-AndroidManifest.xml'
37    );
38    const config: ExpoConfig = {
39      name: 'foo',
40      sdkVersion: '37.0.0',
41      slug: 'my-app',
42      owner: 'owner',
43      updates: {
44        enabled: false,
45        fallbackToCacheTimeout: 2000,
46        checkAutomatically: 'ON_ERROR_RECOVERY',
47        codeSigningCertificate: 'hello',
48        codeSigningMetadata: {
49          alg: 'rsa-v1_5-sha256',
50          keyid: 'test',
51        },
52        requestHeaders: {
53          'expo-channel-name': 'test',
54          testheader: 'test'
55        }
56      },
57    };
58    androidManifestJson = Updates.setUpdatesConfig(
59      '/app',
60      config,
61      androidManifestJson,
62      'user',
63      '0.11.0'
64    );
65    const mainApplication = getMainApplication(androidManifestJson);
66
67    const updateUrl = mainApplication['meta-data'].filter(
68      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL'
69    );
70    expect(updateUrl).toHaveLength(1);
71    expect(updateUrl[0].$['android:value']).toMatch('https://exp.host/@owner/my-app');
72
73    const sdkVersion = mainApplication['meta-data'].filter(
74      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION'
75    );
76    expect(sdkVersion).toHaveLength(1);
77    expect(sdkVersion[0].$['android:value']).toMatch('37.0.0');
78
79    const enabled = mainApplication['meta-data'].filter(
80      (e) => e.$['android:name'] === 'expo.modules.updates.ENABLED'
81    );
82    expect(enabled).toHaveLength(1);
83    expect(enabled[0].$['android:value']).toMatch('false');
84
85    const checkOnLaunch = mainApplication['meta-data'].filter(
86      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH'
87    );
88    expect(checkOnLaunch).toHaveLength(1);
89    expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY');
90
91    const timeout = mainApplication['meta-data'].filter(
92      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS'
93    );
94    expect(timeout).toHaveLength(1);
95    expect(timeout[0].$['android:value']).toMatch('2000');
96
97    const codeSigningCertificate = mainApplication['meta-data'].filter(
98      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE'
99    );
100    expect(codeSigningCertificate).toHaveLength(1);
101    expect(codeSigningCertificate[0].$['android:value']).toMatch(
102      fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8')
103    );
104
105    const codeSigningMetadata = mainApplication['meta-data'].filter(
106      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA'
107    );
108    expect(codeSigningMetadata).toHaveLength(1);
109    expect(codeSigningMetadata[0].$['android:value']).toMatch(
110      '{"alg":"rsa-v1_5-sha256","keyid":"test"}'
111    );
112
113    const requestHeaders = mainApplication['meta-data'].filter(
114      (e) => e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY'
115    );
116    expect(requestHeaders).toHaveLength(1);
117    expect(requestHeaders[0].$['android:value']).toMatch(
118      '{"expo-channel-name":"test","testheader":"test"}'
119    );
120
121    // For this config, runtime version should not be defined, so check that it does not appear in the manifest
122    const runtimeVersion = mainApplication['meta-data']?.filter(
123      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
124    );
125    expect(runtimeVersion).toHaveLength(0);
126  });
127
128  describe(Updates.ensureBuildGradleContainsConfigurationScript, () => {
129    it('adds create-manifest-android.gradle line to build.gradle', async () => {
130      vol.fromJSON(
131        {
132          'android/app/build.gradle': fsReal.readFileSync(
133            path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'),
134            'utf-8'
135          ),
136          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
137        },
138        '/app'
139      );
140
141      const contents = await fs.promises.readFile('/app/android/app/build.gradle', 'utf-8');
142      const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents);
143      expect(newContents).toMatchSnapshot();
144    });
145
146    it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => {
147      // Pseudo node module resolution since actually mocking it could prove challenging.
148      // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked.
149      const resolveFrom = require('resolve-from');
150      resolveFrom.silent = (p, a) => {
151        return silent(path.join(p, '..'), a);
152      };
153
154      vol.fromJSON(
155        {
156          'workspace/android/app/build.gradle': fsReal.readFileSync(
157            path.join(
158              __dirname,
159              'fixtures/build-with-incorrect-create-manifest-android-path.gradle'
160            ),
161            'utf-8'
162          ),
163          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
164        },
165        '/app'
166      );
167
168      const contents = await fs.promises.readFile(
169        '/app/workspace/android/app/build.gradle',
170        'utf-8'
171      );
172      const newContents = Updates.ensureBuildGradleContainsConfigurationScript(
173        '/app/workspace',
174        contents
175      );
176      expect(newContents).toMatchSnapshot();
177    });
178  });
179
180  describe('Runtime version tests', () => {
181    const sampleStringsXML = `
182<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
183<resources>
184</resources>`;
185
186    beforeAll(async () => {
187      const directoryJSON = {
188        './android/app/src/main/res/values/strings.xml': sampleStringsXML,
189      };
190      vol.fromJSON(directoryJSON, '/app');
191    });
192
193    it('Correct metadata written to Android manifest with appVersion policy', async () => {
194      vol.fromJSON({
195        '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'),
196        '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
197      });
198
199      let androidManifestJson = await readAndroidManifestAsync(
200        '/blah/react-native-AndroidManifest.xml'
201      );
202      const config: ExpoConfig = {
203        name: 'foo',
204        version: '37.0.0',
205        slug: 'my-app',
206        owner: 'owner',
207        runtimeVersion: {
208          policy: 'appVersion',
209        },
210      };
211      androidManifestJson = Updates.setUpdatesConfig(
212        '/app',
213        config,
214        androidManifestJson,
215        'user',
216        '0.11.0'
217      );
218      const mainApplication = getMainApplication(androidManifestJson);
219
220      const runtimeVersion = mainApplication['meta-data']?.filter(
221        (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
222      );
223      expect(runtimeVersion).toHaveLength(1);
224      expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch(
225        '@string/expo_runtime_version'
226      );
227    });
228
229    it('Write and clear runtime version in strings resource', async () => {
230      const stringsPath = '/app/android/app/src/main/res/values/strings.xml';
231      const stringsJSON = await readResourcesXMLAsync({ path: stringsPath });
232      const config = {
233        runtimeVersion: '1.10',
234      };
235      Updates.applyRuntimeVersionFromConfig(config, stringsJSON);
236      expect(format(stringsJSON)).toEqual(
237        '<resources>\n  <string name="expo_runtime_version">1.10</string>\n</resources>'
238      );
239
240      const config2 = {
241        sdkVersion: '1.10',
242      };
243      Updates.applyRuntimeVersionFromConfig(config2, stringsJSON);
244      expect(format(stringsJSON)).toEqual('<resources/>');
245    });
246
247    afterAll(async () => {
248      vol.reset();
249    });
250  });
251});
252