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) =>
115        e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY'
116    );
117    expect(requestHeaders).toHaveLength(1);
118    expect(requestHeaders[0].$['android:value']).toMatch(
119      '{"expo-channel-name":"test","testheader":"test"}'
120    );
121
122    // For this config, runtime version should not be defined, so check that it does not appear in the manifest
123    const runtimeVersion = mainApplication['meta-data']?.filter(
124      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
125    );
126    expect(runtimeVersion).toHaveLength(0);
127  });
128
129  describe(Updates.ensureBuildGradleContainsConfigurationScript, () => {
130    it('adds create-manifest-android.gradle line to build.gradle', async () => {
131      vol.fromJSON(
132        {
133          'android/app/build.gradle': fsReal.readFileSync(
134            path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'),
135            'utf-8'
136          ),
137          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
138        },
139        '/app'
140      );
141
142      const contents = await fs.promises.readFile('/app/android/app/build.gradle', 'utf-8');
143      const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents);
144      expect(newContents).toMatchSnapshot();
145    });
146
147    it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => {
148      // Pseudo node module resolution since actually mocking it could prove challenging.
149      // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked.
150      const resolveFrom = require('resolve-from');
151      resolveFrom.silent = (p, a) => {
152        return silent(path.join(p, '..'), a);
153      };
154
155      vol.fromJSON(
156        {
157          'workspace/android/app/build.gradle': fsReal.readFileSync(
158            path.join(
159              __dirname,
160              'fixtures/build-with-incorrect-create-manifest-android-path.gradle'
161            ),
162            'utf-8'
163          ),
164          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
165        },
166        '/app'
167      );
168
169      const contents = await fs.promises.readFile(
170        '/app/workspace/android/app/build.gradle',
171        'utf-8'
172      );
173      const newContents = Updates.ensureBuildGradleContainsConfigurationScript(
174        '/app/workspace',
175        contents
176      );
177      expect(newContents).toMatchSnapshot();
178    });
179  });
180
181  describe('Runtime version tests', () => {
182    const sampleStringsXML = `
183<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
184<resources>
185</resources>`;
186
187    beforeAll(async () => {
188      const directoryJSON = {
189        './android/app/src/main/res/values/strings.xml': sampleStringsXML,
190      };
191      vol.fromJSON(directoryJSON, '/app');
192    });
193
194    it('Correct metadata written to Android manifest with appVersion policy', async () => {
195      vol.fromJSON({
196        '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'),
197        '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
198      });
199
200      let androidManifestJson = await readAndroidManifestAsync(
201        '/blah/react-native-AndroidManifest.xml'
202      );
203      const config: ExpoConfig = {
204        name: 'foo',
205        version: '37.0.0',
206        slug: 'my-app',
207        owner: 'owner',
208        runtimeVersion: {
209          policy: 'appVersion',
210        },
211      };
212      androidManifestJson = Updates.setUpdatesConfig(
213        '/app',
214        config,
215        androidManifestJson,
216        'user',
217        '0.11.0'
218      );
219      const mainApplication = getMainApplication(androidManifestJson);
220
221      const runtimeVersion = mainApplication['meta-data']?.filter(
222        (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
223      );
224      expect(runtimeVersion).toHaveLength(1);
225      expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch(
226        '@string/expo_runtime_version'
227      );
228    });
229
230    it('Write and clear runtime version in strings resource', async () => {
231      const stringsPath = '/app/android/app/src/main/res/values/strings.xml';
232      const stringsJSON = await readResourcesXMLAsync({ path: stringsPath });
233      const config = {
234        runtimeVersion: '1.10',
235      };
236      Updates.applyRuntimeVersionFromConfig(config, stringsJSON);
237      expect(format(stringsJSON)).toEqual(
238        '<resources>\n  <string name="expo_runtime_version">1.10</string>\n</resources>'
239      );
240
241      const config2 = {
242        sdkVersion: '1.10',
243      };
244      Updates.applyRuntimeVersionFromConfig(config2, stringsJSON);
245      expect(format(stringsJSON)).toEqual('<resources/>');
246    });
247
248    afterAll(async () => {
249      vol.reset();
250    });
251  });
252});
253