1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types';
2082815dcSEvan Baconimport fs from 'fs';
3082815dcSEvan Baconimport { vol } from 'memfs';
4082815dcSEvan Baconimport path from 'path';
5082815dcSEvan Bacon
6ed3bd27bSEvan Baconimport rnFixture from '../../plugins/__tests__/fixtures/react-native-project';
7166385e2SDouglas Lowderimport { format } from '../../utils/XML';
8ed3bd27bSEvan Baconimport * as XML from '../../utils/XML';
9ed3bd27bSEvan Baconimport { AndroidManifest, getMainApplication } from '../Manifest';
10166385e2SDouglas Lowderimport { readResourcesXMLAsync } from '../Resources';
11082815dcSEvan Baconimport * as Updates from '../Updates';
12082815dcSEvan Bacon
13ed3bd27bSEvan Baconasync function getFixtureManifestAsync() {
14ed3bd27bSEvan Bacon  return (await XML.parseXMLAsync(
15ed3bd27bSEvan Bacon    rnFixture['android/app/src/main/AndroidManifest.xml']
16ed3bd27bSEvan Bacon  )) as AndroidManifest;
17ed3bd27bSEvan Bacon}
18ed3bd27bSEvan Bacon
19082815dcSEvan Baconconst fixturesPath = path.resolve(__dirname, 'fixtures');
20082815dcSEvan Baconconst sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem');
21082815dcSEvan Bacon
22082815dcSEvan Baconjest.mock('fs');
23082815dcSEvan Baconjest.mock('resolve-from');
24082815dcSEvan Bacon
25082815dcSEvan Baconconst { silent } = require('resolve-from');
26082815dcSEvan Bacon
27082815dcSEvan Baconconst fsReal = jest.requireActual('fs') as typeof import('fs');
28082815dcSEvan Bacon
29082815dcSEvan Bacondescribe('Android Updates config', () => {
30082815dcSEvan Bacon  beforeEach(() => {
31082815dcSEvan Bacon    const resolveFrom = require('resolve-from');
32082815dcSEvan Bacon    resolveFrom.silent = silent;
33082815dcSEvan Bacon    vol.reset();
34082815dcSEvan Bacon  });
35082815dcSEvan Bacon
36082815dcSEvan Bacon  it('set correct values in AndroidManifest.xml', async () => {
37082815dcSEvan Bacon    vol.fromJSON({
38082815dcSEvan Bacon      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
39082815dcSEvan Bacon    });
40082815dcSEvan Bacon
41ed3bd27bSEvan Bacon    let androidManifestJson = await getFixtureManifestAsync();
42082815dcSEvan Bacon    const config: ExpoConfig = {
43082815dcSEvan Bacon      name: 'foo',
44082815dcSEvan Bacon      sdkVersion: '37.0.0',
45082815dcSEvan Bacon      slug: 'my-app',
46082815dcSEvan Bacon      owner: 'owner',
47082815dcSEvan Bacon      updates: {
48082815dcSEvan Bacon        enabled: false,
49082815dcSEvan Bacon        fallbackToCacheTimeout: 2000,
50082815dcSEvan Bacon        checkAutomatically: 'ON_ERROR_RECOVERY',
51082815dcSEvan Bacon        codeSigningCertificate: 'hello',
52082815dcSEvan Bacon        codeSigningMetadata: {
53082815dcSEvan Bacon          alg: 'rsa-v1_5-sha256',
54082815dcSEvan Bacon          keyid: 'test',
55082815dcSEvan Bacon        },
5609bd1012SUmberto Ghio        requestHeaders: {
5709bd1012SUmberto Ghio          'expo-channel-name': 'test',
58fc285a6fSBartosz Kaszubowski          testheader: 'test',
59fc285a6fSBartosz Kaszubowski        },
60082815dcSEvan Bacon      },
61082815dcSEvan Bacon    };
62*f0d67e12SMateus Craveiro    androidManifestJson = await Updates.setUpdatesConfigAsync(
63*f0d67e12SMateus Craveiro      '/app',
64*f0d67e12SMateus Craveiro      config,
65*f0d67e12SMateus Craveiro      androidManifestJson,
66*f0d67e12SMateus Craveiro      '0.11.0'
67*f0d67e12SMateus Craveiro    );
68ed3bd27bSEvan Bacon    const mainApplication = getMainApplication(androidManifestJson)!;
69ed3bd27bSEvan Bacon
70ed3bd27bSEvan Bacon    if (!mainApplication['meta-data']) {
71ed3bd27bSEvan Bacon      throw new Error('No meta-data found in AndroidManifest.xml');
72ed3bd27bSEvan Bacon    }
73082815dcSEvan Bacon
74082815dcSEvan Bacon    const updateUrl = mainApplication['meta-data'].filter(
75082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL'
76082815dcSEvan Bacon    );
772fae8288SWill Schurman    expect(updateUrl).toHaveLength(0);
78082815dcSEvan Bacon
79082815dcSEvan Bacon    const sdkVersion = mainApplication['meta-data'].filter(
80082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION'
81082815dcSEvan Bacon    );
82082815dcSEvan Bacon    expect(sdkVersion).toHaveLength(1);
83082815dcSEvan Bacon    expect(sdkVersion[0].$['android:value']).toMatch('37.0.0');
84082815dcSEvan Bacon
85082815dcSEvan Bacon    const enabled = mainApplication['meta-data'].filter(
86082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.ENABLED'
87082815dcSEvan Bacon    );
88082815dcSEvan Bacon    expect(enabled).toHaveLength(1);
89082815dcSEvan Bacon    expect(enabled[0].$['android:value']).toMatch('false');
90082815dcSEvan Bacon
91082815dcSEvan Bacon    const checkOnLaunch = mainApplication['meta-data'].filter(
92082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH'
93082815dcSEvan Bacon    );
94082815dcSEvan Bacon    expect(checkOnLaunch).toHaveLength(1);
95082815dcSEvan Bacon    expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY');
96082815dcSEvan Bacon
97082815dcSEvan Bacon    const timeout = mainApplication['meta-data'].filter(
98082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS'
99082815dcSEvan Bacon    );
100082815dcSEvan Bacon    expect(timeout).toHaveLength(1);
101082815dcSEvan Bacon    expect(timeout[0].$['android:value']).toMatch('2000');
102082815dcSEvan Bacon
103082815dcSEvan Bacon    const codeSigningCertificate = mainApplication['meta-data'].filter(
104082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE'
105082815dcSEvan Bacon    );
106082815dcSEvan Bacon    expect(codeSigningCertificate).toHaveLength(1);
107082815dcSEvan Bacon    expect(codeSigningCertificate[0].$['android:value']).toMatch(
108082815dcSEvan Bacon      fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8')
109082815dcSEvan Bacon    );
110082815dcSEvan Bacon
111082815dcSEvan Bacon    const codeSigningMetadata = mainApplication['meta-data'].filter(
112082815dcSEvan Bacon      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA'
113082815dcSEvan Bacon    );
114082815dcSEvan Bacon    expect(codeSigningMetadata).toHaveLength(1);
115082815dcSEvan Bacon    expect(codeSigningMetadata[0].$['android:value']).toMatch(
116082815dcSEvan Bacon      '{"alg":"rsa-v1_5-sha256","keyid":"test"}'
117082815dcSEvan Bacon    );
118166385e2SDouglas Lowder
11909bd1012SUmberto Ghio    const requestHeaders = mainApplication['meta-data'].filter(
120fc285a6fSBartosz Kaszubowski      (e) =>
121fc285a6fSBartosz Kaszubowski        e.$['android:name'] === 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY'
12209bd1012SUmberto Ghio    );
12309bd1012SUmberto Ghio    expect(requestHeaders).toHaveLength(1);
12409bd1012SUmberto Ghio    expect(requestHeaders[0].$['android:value']).toMatch(
12509bd1012SUmberto Ghio      '{"expo-channel-name":"test","testheader":"test"}'
12609bd1012SUmberto Ghio    );
12709bd1012SUmberto Ghio
128166385e2SDouglas Lowder    // For this config, runtime version should not be defined, so check that it does not appear in the manifest
129166385e2SDouglas Lowder    const runtimeVersion = mainApplication['meta-data']?.filter(
130166385e2SDouglas Lowder      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
131166385e2SDouglas Lowder    );
132166385e2SDouglas Lowder    expect(runtimeVersion).toHaveLength(0);
133082815dcSEvan Bacon  });
134082815dcSEvan Bacon
135082815dcSEvan Bacon  describe(Updates.ensureBuildGradleContainsConfigurationScript, () => {
136082815dcSEvan Bacon    it('adds create-manifest-android.gradle line to build.gradle', async () => {
137082815dcSEvan Bacon      vol.fromJSON(
138082815dcSEvan Bacon        {
139082815dcSEvan Bacon          'android/app/build.gradle': fsReal.readFileSync(
140082815dcSEvan Bacon            path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'),
141082815dcSEvan Bacon            'utf-8'
142082815dcSEvan Bacon          ),
143082815dcSEvan Bacon          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
144082815dcSEvan Bacon        },
145082815dcSEvan Bacon        '/app'
146082815dcSEvan Bacon      );
147082815dcSEvan Bacon
148ed3bd27bSEvan Bacon      const contents = rnFixture['android/app/build.gradle'];
149082815dcSEvan Bacon      const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents);
150082815dcSEvan Bacon      expect(newContents).toMatchSnapshot();
151082815dcSEvan Bacon    });
152082815dcSEvan Bacon
153082815dcSEvan Bacon    it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => {
154082815dcSEvan Bacon      // Pseudo node module resolution since actually mocking it could prove challenging.
155082815dcSEvan Bacon      // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked.
156082815dcSEvan Bacon      const resolveFrom = require('resolve-from');
157082815dcSEvan Bacon      resolveFrom.silent = (p, a) => {
158082815dcSEvan Bacon        return silent(path.join(p, '..'), a);
159082815dcSEvan Bacon      };
160082815dcSEvan Bacon
161082815dcSEvan Bacon      vol.fromJSON(
162082815dcSEvan Bacon        {
163082815dcSEvan Bacon          'workspace/android/app/build.gradle': fsReal.readFileSync(
164082815dcSEvan Bacon            path.join(
165082815dcSEvan Bacon              __dirname,
166082815dcSEvan Bacon              'fixtures/build-with-incorrect-create-manifest-android-path.gradle'
167082815dcSEvan Bacon            ),
168082815dcSEvan Bacon            'utf-8'
169082815dcSEvan Bacon          ),
170082815dcSEvan Bacon          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
171082815dcSEvan Bacon        },
172082815dcSEvan Bacon        '/app'
173082815dcSEvan Bacon      );
174082815dcSEvan Bacon
175082815dcSEvan Bacon      const contents = await fs.promises.readFile(
176082815dcSEvan Bacon        '/app/workspace/android/app/build.gradle',
177082815dcSEvan Bacon        'utf-8'
178082815dcSEvan Bacon      );
179082815dcSEvan Bacon      const newContents = Updates.ensureBuildGradleContainsConfigurationScript(
180082815dcSEvan Bacon        '/app/workspace',
181082815dcSEvan Bacon        contents
182082815dcSEvan Bacon      );
183082815dcSEvan Bacon      expect(newContents).toMatchSnapshot();
184082815dcSEvan Bacon    });
185082815dcSEvan Bacon  });
186166385e2SDouglas Lowder
187166385e2SDouglas Lowder  describe('Runtime version tests', () => {
188166385e2SDouglas Lowder    const sampleStringsXML = `
189166385e2SDouglas Lowder<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
190166385e2SDouglas Lowder<resources>
191166385e2SDouglas Lowder</resources>`;
192166385e2SDouglas Lowder
193166385e2SDouglas Lowder    beforeAll(async () => {
194166385e2SDouglas Lowder      const directoryJSON = {
195166385e2SDouglas Lowder        './android/app/src/main/res/values/strings.xml': sampleStringsXML,
196166385e2SDouglas Lowder      };
197166385e2SDouglas Lowder      vol.fromJSON(directoryJSON, '/app');
198166385e2SDouglas Lowder    });
199166385e2SDouglas Lowder
200166385e2SDouglas Lowder    it('Correct metadata written to Android manifest with appVersion policy', async () => {
201166385e2SDouglas Lowder      vol.fromJSON({
202166385e2SDouglas Lowder        '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
203166385e2SDouglas Lowder      });
204166385e2SDouglas Lowder
205ed3bd27bSEvan Bacon      let androidManifestJson = await getFixtureManifestAsync();
206166385e2SDouglas Lowder      const config: ExpoConfig = {
207166385e2SDouglas Lowder        name: 'foo',
208166385e2SDouglas Lowder        version: '37.0.0',
209166385e2SDouglas Lowder        slug: 'my-app',
210166385e2SDouglas Lowder        owner: 'owner',
211166385e2SDouglas Lowder        runtimeVersion: {
212166385e2SDouglas Lowder          policy: 'appVersion',
213166385e2SDouglas Lowder        },
214166385e2SDouglas Lowder      };
215*f0d67e12SMateus Craveiro      androidManifestJson = await Updates.setUpdatesConfigAsync(
216*f0d67e12SMateus Craveiro        '/app',
217*f0d67e12SMateus Craveiro        config,
218*f0d67e12SMateus Craveiro        androidManifestJson,
219*f0d67e12SMateus Craveiro        '0.11.0'
220*f0d67e12SMateus Craveiro      );
221166385e2SDouglas Lowder      const mainApplication = getMainApplication(androidManifestJson);
222166385e2SDouglas Lowder
223ed3bd27bSEvan Bacon      const runtimeVersion = mainApplication!['meta-data']?.filter(
224166385e2SDouglas Lowder        (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_RUNTIME_VERSION'
225166385e2SDouglas Lowder      );
226166385e2SDouglas Lowder      expect(runtimeVersion).toHaveLength(1);
227166385e2SDouglas Lowder      expect(runtimeVersion && runtimeVersion[0].$['android:value']).toMatch(
228166385e2SDouglas Lowder        '@string/expo_runtime_version'
229166385e2SDouglas Lowder      );
230166385e2SDouglas Lowder    });
231166385e2SDouglas Lowder
232166385e2SDouglas Lowder    it('Write and clear runtime version in strings resource', async () => {
233166385e2SDouglas Lowder      const stringsPath = '/app/android/app/src/main/res/values/strings.xml';
234166385e2SDouglas Lowder      const stringsJSON = await readResourcesXMLAsync({ path: stringsPath });
235166385e2SDouglas Lowder      const config = {
236166385e2SDouglas Lowder        runtimeVersion: '1.10',
237*f0d67e12SMateus Craveiro        modRequest: {
238*f0d67e12SMateus Craveiro          projectRoot: '/',
239*f0d67e12SMateus Craveiro        },
240166385e2SDouglas Lowder      };
241*f0d67e12SMateus Craveiro      await Updates.applyRuntimeVersionFromConfigAsync(config, stringsJSON);
242166385e2SDouglas Lowder      expect(format(stringsJSON)).toEqual(
243166385e2SDouglas Lowder        '<resources>\n  <string name="expo_runtime_version">1.10</string>\n</resources>'
244166385e2SDouglas Lowder      );
245166385e2SDouglas Lowder
246166385e2SDouglas Lowder      const config2 = {
247166385e2SDouglas Lowder        sdkVersion: '1.10',
248*f0d67e12SMateus Craveiro        modRequest: {
249*f0d67e12SMateus Craveiro          projectRoot: '/',
250*f0d67e12SMateus Craveiro        },
251166385e2SDouglas Lowder      };
252*f0d67e12SMateus Craveiro      await Updates.applyRuntimeVersionFromConfigAsync(config2, stringsJSON);
253166385e2SDouglas Lowder      expect(format(stringsJSON)).toEqual('<resources/>');
254166385e2SDouglas Lowder    });
255166385e2SDouglas Lowder
256166385e2SDouglas Lowder    afterAll(async () => {
257166385e2SDouglas Lowder      vol.reset();
258166385e2SDouglas Lowder    });
259166385e2SDouglas Lowder  });
260082815dcSEvan Bacon});
261