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