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