1import fs from 'fs';
2import { vol } from 'memfs';
3import path from 'path';
4
5import {
6  getNativeVersion,
7  getRuntimeVersionAsync,
8  getSDKVersion,
9  getUpdatesCheckOnLaunch,
10  getUpdatesCodeSigningCertificate,
11  getUpdatesCodeSigningMetadata,
12  getUpdatesCodeSigningMetadataStringified,
13  getUpdatesRequestHeaders,
14  getUpdatesRequestHeadersStringified,
15  getUpdatesEnabled,
16  getUpdatesTimeout,
17  getUpdateUrl,
18} from '../Updates';
19
20const fsReal = jest.requireActual('fs') as typeof fs;
21jest.mock('fs');
22jest.mock('resolve-from');
23
24const { silent } = require('resolve-from');
25
26const fixturesPath = path.resolve(__dirname, 'fixtures');
27const sampleCodeSigningCertificatePath = path.resolve(fixturesPath, 'codeSigningCertificate.pem');
28
29console.warn = jest.fn();
30
31describe('shared config getters', () => {
32  beforeEach(() => {
33    const resolveFrom = require('resolve-from');
34    resolveFrom.silent = silent;
35    vol.reset();
36  });
37
38  it(`returns correct default values from all getters if no value provided`, () => {
39    expect(getSDKVersion({})).toBe(null);
40    expect(getUpdatesCheckOnLaunch({})).toBe('ALWAYS');
41    expect(getUpdatesTimeout({})).toBe(0);
42    expect(getUpdatesCodeSigningCertificate('/app', {})).toBe(undefined);
43    expect(getUpdatesCodeSigningMetadata({})).toBe(undefined);
44    expect(getUpdatesRequestHeaders({})).toBe(undefined);
45
46    expect(getUpdatesEnabled({})).toBe(false);
47    expect(getUpdatesEnabled({ updates: {} })).toBe(false);
48  });
49
50  it(`returns correct value from all getters if value provided`, () => {
51    vol.fromJSON({
52      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
53    });
54
55    expect(getSDKVersion({ sdkVersion: '37.0.0' })).toBe('37.0.0');
56    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } })).toBe(
57      'NEVER'
58    );
59    expect(
60      getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } }, '0.11.0')
61    ).toBe('ERROR_RECOVERY_ONLY');
62    expect(
63      getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } }, '0.10.15')
64    ).toBe('NEVER');
65    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_LOAD' } })).toBe('ALWAYS');
66    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'WIFI_ONLY' } })).toBe(
67      'WIFI_ONLY'
68    );
69    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'NEVER' } })).toBe('NEVER');
70    expect(getUpdatesCheckOnLaunch({ updates: {} })).toBe('ALWAYS');
71    expect(getUpdatesEnabled({ updates: { enabled: false } })).toBe(false);
72    expect(getUpdatesTimeout({ updates: { fallbackToCacheTimeout: 2000 } })).toBe(2000);
73    expect(
74      getUpdatesCodeSigningCertificate('/app', {
75        updates: {
76          codeSigningCertificate: 'hello',
77        },
78      })
79    ).toBe(fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'));
80    expect(
81      getUpdatesCodeSigningMetadataStringified({
82        updates: {
83          codeSigningMetadata: {
84            alg: 'rsa-v1_5-sha256',
85            keyid: 'test',
86          },
87        },
88      })
89    ).toBe(
90      JSON.stringify({
91        alg: 'rsa-v1_5-sha256',
92        keyid: 'test',
93      })
94    );
95    expect(
96      getUpdatesCodeSigningMetadata({
97        updates: {
98          codeSigningMetadata: {
99            alg: 'rsa-v1_5-sha256',
100            keyid: 'test',
101          },
102        },
103      })
104    ).toMatchObject({
105      alg: 'rsa-v1_5-sha256',
106      keyid: 'test',
107    });
108    expect(
109      getUpdatesRequestHeadersStringified({
110        updates: {
111          requestHeaders: {
112            'expo-channel-name': 'test',
113            testheader: 'test',
114          },
115        },
116      })
117    ).toBe(
118      JSON.stringify({
119        'expo-channel-name': 'test',
120        testheader: 'test',
121      })
122    );
123    expect(
124      getUpdatesRequestHeaders({
125        updates: {
126          requestHeaders: {
127            'expo-channel-name': 'test',
128            testheader: 'test',
129          },
130        },
131      })
132    ).toMatchObject({
133      'expo-channel-name': 'test',
134      testheader: 'test',
135    });
136  });
137});
138
139describe(getUpdateUrl, () => {
140  it(`returns correct default values from all getters if no value provided.`, () => {
141    const url = 'https://u.expo.dev/00000000-0000-0000-0000-000000000000';
142    expect(getUpdateUrl({ updates: { url } })).toBe(url);
143  });
144
145  it(`returns correct legacy urls if 'updates.url' is not provided, but 'slug' and ('username'|'owner') are provided and useClassicUpdates is false.`, () => {
146    expect(getUpdateUrl({})).toBe(null);
147  });
148});
149
150describe(getNativeVersion, () => {
151  const version = '2.0.0';
152  const versionCode = 42;
153  const buildNumber = '13';
154  it('works for android', () => {
155    expect(getNativeVersion({ version, android: { versionCode } }, 'android')).toBe(
156      `${version}(${versionCode})`
157    );
158  });
159  it('works for ios', () => {
160    expect(getNativeVersion({ version, ios: { buildNumber } }, 'ios')).toBe(
161      `${version}(${buildNumber})`
162    );
163  });
164  it('throws an error if platform is not recognized', () => {
165    const fakePlatform = 'doesnotexist';
166    expect(() => {
167      getNativeVersion({ version }, fakePlatform as any);
168    }).toThrow(`"${fakePlatform}" is not a supported platform. Choose either "ios" or "android".`);
169  });
170  it('uses the default version if the version is missing', () => {
171    expect(getNativeVersion({}, 'ios')).toBe('1.0.0(1)');
172  });
173  it('uses the default buildNumber if the platform is ios and the buildNumber is missing', () => {
174    expect(getNativeVersion({ version }, 'ios')).toBe(`${version}(1)`);
175  });
176  it('uses the default versionCode if the platform is android and the versionCode is missing', () => {
177    expect(getNativeVersion({ version }, 'android')).toBe(`${version}(1)`);
178  });
179});
180
181describe(getRuntimeVersionAsync, () => {
182  it('works if the top level runtimeVersion is a string', async () => {
183    const runtimeVersion = '42';
184    expect(await getRuntimeVersionAsync('', { runtimeVersion }, 'ios')).toBe(runtimeVersion);
185  });
186  it('works if the platform specific runtimeVersion is a string', async () => {
187    const runtimeVersion = '42';
188    expect(await getRuntimeVersionAsync('', { ios: { runtimeVersion } }, 'ios')).toBe(
189      runtimeVersion
190    );
191  });
192  it('works if the runtimeVersion is a nativeVersion policy', async () => {
193    const version = '1';
194    const buildNumber = '2';
195    expect(
196      await getRuntimeVersionAsync(
197        '',
198        { version, runtimeVersion: { policy: 'nativeVersion' }, ios: { buildNumber } },
199        'ios'
200      )
201    ).toBe(`${version}(${buildNumber})`);
202  });
203  it('works if the runtimeVersion is an appVersion policy', async () => {
204    const version = '1';
205    const buildNumber = '2';
206    expect(
207      await getRuntimeVersionAsync(
208        '',
209        { version, runtimeVersion: { policy: 'appVersion' }, ios: { buildNumber } },
210        'ios'
211      )
212    ).toBe(version);
213  });
214  it('returns null if no runtime version is supplied', async () => {
215    expect(await getRuntimeVersionAsync('', {}, 'ios')).toEqual(null);
216  });
217  it('throws if runtime version is not parseable', async () => {
218    await expect(getRuntimeVersionAsync('', { runtimeVersion: 1 } as any, 'ios')).rejects.toThrow(
219      `"1" is not a valid runtime version. getRuntimeVersionAsync only supports a string, "sdkVersion", "appVersion", "nativeVersion" or "fingerprintExperimental" policy.`
220    );
221    await expect(
222      getRuntimeVersionAsync('', { runtimeVersion: { policy: 'unsupportedPlugin' } } as any, 'ios')
223    ).rejects.toThrow(
224      `"{"policy":"unsupportedPlugin"}" is not a valid runtime version. getRuntimeVersionAsync only supports a string, "sdkVersion", "appVersion", "nativeVersion" or "fingerprintExperimental" policy.`
225    );
226  });
227});
228