1import fs from 'fs';
2import { vol } from 'memfs';
3import path from 'path';
4
5import {
6  getNativeVersion,
7  getRuntimeVersion,
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(getRuntimeVersion, () => {
182  it('works if the top level runtimeVersion is a string', () => {
183    const runtimeVersion = '42';
184    expect(getRuntimeVersion({ runtimeVersion }, 'ios')).toBe(runtimeVersion);
185  });
186  it('works if the platform specific runtimeVersion is a string', () => {
187    const runtimeVersion = '42';
188    expect(getRuntimeVersion({ ios: { runtimeVersion } }, 'ios')).toBe(runtimeVersion);
189  });
190  it('works if the runtimeVersion is a nativeVersion policy', () => {
191    const version = '1';
192    const buildNumber = '2';
193    expect(
194      getRuntimeVersion(
195        { version, runtimeVersion: { policy: 'nativeVersion' }, ios: { buildNumber } },
196        'ios'
197      )
198    ).toBe(`${version}(${buildNumber})`);
199  });
200  it('works if the runtimeVersion is an appVersion policy', () => {
201    const version = '1';
202    const buildNumber = '2';
203    expect(
204      getRuntimeVersion(
205        { version, runtimeVersion: { policy: 'appVersion' }, ios: { buildNumber } },
206        'ios'
207      )
208    ).toBe(version);
209  });
210  it('returns null if no runtime version is supplied', () => {
211    expect(getRuntimeVersion({}, 'ios')).toEqual(null);
212  });
213  it('throws if runtime version is not parseable', () => {
214    expect(() => {
215      getRuntimeVersion({ runtimeVersion: 1 } as any, 'ios');
216    }).toThrow(
217      `"1" is not a valid runtime version. getRuntimeVersion only supports a string, "sdkVersion", "appVersion", or "nativeVersion" policy.`
218    );
219    expect(() => {
220      getRuntimeVersion({ runtimeVersion: { policy: 'unsupportedPlugin' } } as any, 'ios');
221    }).toThrow(
222      `"{"policy":"unsupportedPlugin"}" is not a valid runtime version. getRuntimeVersion only supports a string, "sdkVersion", "appVersion", or "nativeVersion" policy.`
223    );
224  });
225});
226