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({ slug: 'my-app', owner: 'owner' }, null)).toBe(false);
47    expect(getUpdatesEnabled({ slug: 'my-app', owner: 'owner' }, 'owner')).toBe(false);
48    expect(
49      getUpdatesEnabled(
50        { slug: 'my-app', owner: 'owner', updates: { useClassicUpdates: true } },
51        null
52      )
53    ).toBe(true);
54    expect(
55      getUpdatesEnabled(
56        { slug: 'my-app', owner: 'owner', updates: { useClassicUpdates: true } },
57        'owner'
58      )
59    ).toBe(true);
60    expect(
61      getUpdatesEnabled(
62        { slug: 'my-app', owner: undefined, updates: { useClassicUpdates: true } },
63        null
64      )
65    ).toBe(false);
66  });
67
68  it(`returns correct value from all getters if value provided`, () => {
69    vol.fromJSON({
70      '/app/hello': fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'),
71    });
72
73    expect(getSDKVersion({ sdkVersion: '37.0.0' })).toBe('37.0.0');
74    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } })).toBe(
75      'NEVER'
76    );
77    expect(
78      getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } }, '0.11.0')
79    ).toBe('ERROR_RECOVERY_ONLY');
80    expect(
81      getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_ERROR_RECOVERY' } }, '0.10.15')
82    ).toBe('NEVER');
83    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'ON_LOAD' } })).toBe('ALWAYS');
84    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'WIFI_ONLY' } })).toBe(
85      'WIFI_ONLY'
86    );
87    expect(getUpdatesCheckOnLaunch({ updates: { checkAutomatically: 'NEVER' } })).toBe('NEVER');
88    expect(getUpdatesCheckOnLaunch({ updates: {} })).toBe('ALWAYS');
89    expect(
90      getUpdatesEnabled({ slug: 'my-app', owner: 'owner', updates: { enabled: false } }, null)
91    ).toBe(false);
92    expect(getUpdatesTimeout({ updates: { fallbackToCacheTimeout: 2000 } })).toBe(2000);
93    expect(
94      getUpdatesCodeSigningCertificate('/app', {
95        updates: {
96          codeSigningCertificate: 'hello',
97        },
98      })
99    ).toBe(fsReal.readFileSync(sampleCodeSigningCertificatePath, 'utf-8'));
100    expect(
101      getUpdatesCodeSigningMetadataStringified({
102        updates: {
103          codeSigningMetadata: {
104            alg: 'rsa-v1_5-sha256',
105            keyid: 'test',
106          },
107        },
108      })
109    ).toBe(
110      JSON.stringify({
111        alg: 'rsa-v1_5-sha256',
112        keyid: 'test',
113      })
114    );
115    expect(
116      getUpdatesCodeSigningMetadata({
117        updates: {
118          codeSigningMetadata: {
119            alg: 'rsa-v1_5-sha256',
120            keyid: 'test',
121          },
122        },
123      })
124    ).toMatchObject({
125      alg: 'rsa-v1_5-sha256',
126      keyid: 'test',
127    });
128    expect(
129      getUpdatesRequestHeadersStringified({
130        updates: {
131          requestHeaders: {
132            'expo-channel-name': 'test',
133            testheader: 'test',
134          },
135        },
136      })
137    ).toBe(
138      JSON.stringify({
139        'expo-channel-name': 'test',
140        testheader: 'test',
141      })
142    );
143    expect(
144      getUpdatesRequestHeaders({
145        updates: {
146          requestHeaders: {
147            'expo-channel-name': 'test',
148            testheader: 'test',
149          },
150        },
151      })
152    ).toMatchObject({
153      'expo-channel-name': 'test',
154      testheader: 'test',
155    });
156  });
157});
158
159describe(getUpdateUrl, () => {
160  it(`returns correct default values from all getters if no value provided.`, () => {
161    const url = 'https://u.expo.dev/00000000-0000-0000-0000-000000000000';
162    expect(getUpdateUrl({ updates: { url }, slug: 'foo' }, 'user')).toBe(url);
163  });
164
165  it(`returns null if neither 'updates.url' or 'user' is supplied.`, () => {
166    expect(getUpdateUrl({ slug: 'foo' }, null)).toBe(null);
167  });
168
169  it(`returns correct legacy urls if 'updates.url' is not provided, but 'slug' and ('username'|'owner') are provided and useClassicUpdates is true.`, () => {
170    expect(getUpdateUrl({ slug: 'my-app', updates: { useClassicUpdates: true } }, 'user')).toBe(
171      'https://exp.host/@user/my-app'
172    );
173    expect(
174      getUpdateUrl({ slug: 'my-app', owner: 'owner', updates: { useClassicUpdates: true } }, 'user')
175    ).toBe('https://exp.host/@owner/my-app');
176    expect(
177      getUpdateUrl({ slug: 'my-app', owner: 'owner', updates: { useClassicUpdates: true } }, null)
178    ).toBe('https://exp.host/@owner/my-app');
179  });
180
181  it(`returns correct legacy urls if 'updates.url' is not provided, but 'slug' and ('username'|'owner') are provided and useClassicUpdates is false.`, () => {
182    expect(getUpdateUrl({ slug: 'my-app' }, 'user')).toBe(null);
183    expect(getUpdateUrl({ slug: 'my-app', owner: 'owner' }, 'user')).toBe(null);
184    expect(getUpdateUrl({ slug: 'my-app', owner: 'owner' }, null)).toBe(null);
185  });
186});
187
188describe(getNativeVersion, () => {
189  const version = '2.0.0';
190  const versionCode = 42;
191  const buildNumber = '13';
192  it('works for android', () => {
193    expect(getNativeVersion({ version, android: { versionCode } }, 'android')).toBe(
194      `${version}(${versionCode})`
195    );
196  });
197  it('works for ios', () => {
198    expect(getNativeVersion({ version, ios: { buildNumber } }, 'ios')).toBe(
199      `${version}(${buildNumber})`
200    );
201  });
202  it('throws an error if platform is not recognized', () => {
203    const fakePlatform = 'doesnotexist';
204    expect(() => {
205      getNativeVersion({ version }, fakePlatform as any);
206    }).toThrow(`"${fakePlatform}" is not a supported platform. Choose either "ios" or "android".`);
207  });
208  it('uses the default version if the version is missing', () => {
209    expect(getNativeVersion({}, 'ios')).toBe('1.0.0(1)');
210  });
211  it('uses the default buildNumber if the platform is ios and the buildNumber is missing', () => {
212    expect(getNativeVersion({ version }, 'ios')).toBe(`${version}(1)`);
213  });
214  it('uses the default versionCode if the platform is android and the versionCode is missing', () => {
215    expect(getNativeVersion({ version }, 'android')).toBe(`${version}(1)`);
216  });
217});
218
219describe(getRuntimeVersion, () => {
220  it('works if the top level runtimeVersion is a string', () => {
221    const runtimeVersion = '42';
222    expect(getRuntimeVersion({ runtimeVersion }, 'ios')).toBe(runtimeVersion);
223  });
224  it('works if the platform specific runtimeVersion is a string', () => {
225    const runtimeVersion = '42';
226    expect(getRuntimeVersion({ ios: { runtimeVersion } }, 'ios')).toBe(runtimeVersion);
227  });
228  it('works if the runtimeVersion is a nativeVersion policy', () => {
229    const version = '1';
230    const buildNumber = '2';
231    expect(
232      getRuntimeVersion(
233        { version, runtimeVersion: { policy: 'nativeVersion' }, ios: { buildNumber } },
234        'ios'
235      )
236    ).toBe(`${version}(${buildNumber})`);
237  });
238  it('works if the runtimeVersion is an appVersion policy', () => {
239    const version = '1';
240    const buildNumber = '2';
241    expect(
242      getRuntimeVersion(
243        { version, runtimeVersion: { policy: 'appVersion' }, ios: { buildNumber } },
244        'ios'
245      )
246    ).toBe(version);
247  });
248  it('returns null if no runtime version is supplied', () => {
249    expect(getRuntimeVersion({}, 'ios')).toEqual(null);
250  });
251  it('throws if runtime version is not parseable', () => {
252    expect(() => {
253      getRuntimeVersion({ runtimeVersion: 1 } as any, 'ios');
254    }).toThrow(
255      `"1" is not a valid runtime version. getRuntimeVersion only supports a string, "sdkVersion", "appVersion", or "nativeVersion" policy.`
256    );
257    expect(() => {
258      getRuntimeVersion({ runtimeVersion: { policy: 'unsupportedPlugin' } } as any, 'ios');
259    }).toThrow(
260      `"{"policy":"unsupportedPlugin"}" is not a valid runtime version. getRuntimeVersion only supports a string, "sdkVersion", "appVersion", or "nativeVersion" policy.`
261    );
262  });
263});
264