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