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