1import { ExpoConfig } from '@expo/config-types';
2import fs from 'fs';
3import { vol } from 'memfs';
4import path from 'path';
5
6import { getMainApplication, readAndroidManifestAsync } from '../Manifest';
7import * as Updates from '../Updates';
8
9const fixturesPath = path.resolve(__dirname, 'fixtures');
10const sampleManifestPath = path.resolve(fixturesPath, 'react-native-AndroidManifest.xml');
11const sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem');
12
13jest.mock('fs');
14jest.mock('resolve-from');
15
16const { silent } = require('resolve-from');
17
18const fsReal = jest.requireActual('fs') as typeof import('fs');
19
20describe('Android Updates config', () => {
21  beforeEach(() => {
22    const resolveFrom = require('resolve-from');
23    resolveFrom.silent = silent;
24    vol.reset();
25  });
26
27  it('set correct values in AndroidManifest.xml', async () => {
28    vol.fromJSON({
29      '/blah/react-native-AndroidManifest.xml': fsReal.readFileSync(sampleManifestPath, 'utf-8'),
30      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
31    });
32
33    let androidManifestJson = await readAndroidManifestAsync(
34      '/blah/react-native-AndroidManifest.xml'
35    );
36    const config: ExpoConfig = {
37      name: 'foo',
38      sdkVersion: '37.0.0',
39      slug: 'my-app',
40      owner: 'owner',
41      updates: {
42        enabled: false,
43        fallbackToCacheTimeout: 2000,
44        checkAutomatically: 'ON_ERROR_RECOVERY',
45        codeSigningCertificate: 'hello',
46        codeSigningMetadata: {
47          alg: 'rsa-v1_5-sha256',
48          keyid: 'test',
49        },
50      },
51    };
52    androidManifestJson = Updates.setUpdatesConfig(
53      '/app',
54      config,
55      androidManifestJson,
56      'user',
57      '0.11.0'
58    );
59    const mainApplication = getMainApplication(androidManifestJson);
60
61    const updateUrl = mainApplication['meta-data'].filter(
62      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATE_URL'
63    );
64    expect(updateUrl).toHaveLength(1);
65    expect(updateUrl[0].$['android:value']).toMatch('https://exp.host/@owner/my-app');
66
67    const sdkVersion = mainApplication['meta-data'].filter(
68      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_SDK_VERSION'
69    );
70    expect(sdkVersion).toHaveLength(1);
71    expect(sdkVersion[0].$['android:value']).toMatch('37.0.0');
72
73    const enabled = mainApplication['meta-data'].filter(
74      (e) => e.$['android:name'] === 'expo.modules.updates.ENABLED'
75    );
76    expect(enabled).toHaveLength(1);
77    expect(enabled[0].$['android:value']).toMatch('false');
78
79    const checkOnLaunch = mainApplication['meta-data'].filter(
80      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH'
81    );
82    expect(checkOnLaunch).toHaveLength(1);
83    expect(checkOnLaunch[0].$['android:value']).toMatch('ERROR_RECOVERY_ONLY');
84
85    const timeout = mainApplication['meta-data'].filter(
86      (e) => e.$['android:name'] === 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS'
87    );
88    expect(timeout).toHaveLength(1);
89    expect(timeout[0].$['android:value']).toMatch('2000');
90
91    const codeSigningCertificate = mainApplication['meta-data'].filter(
92      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_CERTIFICATE'
93    );
94    expect(codeSigningCertificate).toHaveLength(1);
95    expect(codeSigningCertificate[0].$['android:value']).toMatch(
96      fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8')
97    );
98
99    const codeSigningMetadata = mainApplication['meta-data'].filter(
100      (e) => e.$['android:name'] === 'expo.modules.updates.CODE_SIGNING_METADATA'
101    );
102    expect(codeSigningMetadata).toHaveLength(1);
103    expect(codeSigningMetadata[0].$['android:value']).toMatch(
104      '{"alg":"rsa-v1_5-sha256","keyid":"test"}'
105    );
106  });
107
108  describe(Updates.ensureBuildGradleContainsConfigurationScript, () => {
109    it('adds create-manifest-android.gradle line to build.gradle', async () => {
110      vol.fromJSON(
111        {
112          'android/app/build.gradle': fsReal.readFileSync(
113            path.join(__dirname, 'fixtures/build-without-create-manifest-android.gradle'),
114            'utf-8'
115          ),
116          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
117        },
118        '/app'
119      );
120
121      const contents = await fs.promises.readFile('/app/android/app/build.gradle', 'utf-8');
122      const newContents = Updates.ensureBuildGradleContainsConfigurationScript('/app', contents);
123      expect(newContents).toMatchSnapshot();
124    });
125
126    it('fixes the path to create-manifest-android.gradle in case of a monorepo', async () => {
127      // Pseudo node module resolution since actually mocking it could prove challenging.
128      // In a yarn workspace, resolve-from would be able to locate a module in any node_module folder if properly linked.
129      const resolveFrom = require('resolve-from');
130      resolveFrom.silent = (p, a) => {
131        return silent(path.join(p, '..'), a);
132      };
133
134      vol.fromJSON(
135        {
136          'workspace/android/app/build.gradle': fsReal.readFileSync(
137            path.join(
138              __dirname,
139              'fixtures/build-with-incorrect-create-manifest-android-path.gradle'
140            ),
141            'utf-8'
142          ),
143          'node_modules/expo-updates/scripts/create-manifest-android.gradle': 'whatever',
144        },
145        '/app'
146      );
147
148      const contents = await fs.promises.readFile(
149        '/app/workspace/android/app/build.gradle',
150        'utf-8'
151      );
152      const newContents = Updates.ensureBuildGradleContainsConfigurationScript(
153        '/app/workspace',
154        contents
155      );
156      expect(newContents).toMatchSnapshot();
157    });
158  });
159});
160