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