1/* eslint-env jest */
2import fs from 'fs-extra';
3import os from 'os';
4import path from 'path';
5import stripAnsi from 'strip-ansi';
6
7import { mockSpawnPromise, mockedSpawnAsync } from '../../__tests__/spawn-utils';
8import {
9  CocoaPodsPackageManager,
10  getPodRepoUpdateMessage,
11  getPodUpdateMessage,
12} from '../CocoaPodsPackageManager';
13
14const projectRoot = getTemporaryPath();
15
16function getTemporaryPath() {
17  return path.join(os.tmpdir(), Math.random().toString(36).substring(2));
18}
19function getRoot(...args) {
20  return path.join(projectRoot, ...args);
21}
22
23jest.mock('@expo/spawn-async');
24
25const originalForceColor = process.env.FORCE_COLOR;
26
27beforeEach(() => {
28  process.env.FORCE_COLOR = '1';
29});
30
31afterAll(() => {
32  process.env.FORCE_COLOR = originalForceColor;
33});
34
35const fakePodRepoUpdateErrorOutput = {
36  pid: 74312,
37  // prettier-ignore
38  output: [
39    'Using Expo modules' + os.EOL +
40      'Auto-linking React Native modules for target `yolo74`: RNGestureHandler, RNReanimated, RNScreens, and react-native-safe-area-context' + os.EOL +
41      'Analyzing dependencies' + os.EOL +
42      '[!] CocoaPods could not find compatible versions for pod "EXFileSystem":' + os.EOL +
43      '  In snapshot (Podfile.lock):' + os.EOL +
44      '    EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL +
45      '' + os.EOL +
46      '  In Podfile:' + os.EOL +
47      '    EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL +
48      '' + os.EOL +
49      '' + os.EOL +
50      'You have either:' + os.EOL +
51      ' * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.' + os.EOL +
52      ' * changed the constraints of dependency `EXFileSystem` inside your development pod `EXFileSystem`.' + os.EOL +
53      "   You should run `pod update EXFileSystem` to apply changes you've made.\n",
54    'Ignoring ffi-1.13.1 because its extensions are not built. Try: gem pristine ffi --version 1.13.1\n',
55  ],
56  // prettier-ignore
57  stdout:
58    'Using Expo modules' + os.EOL +
59    'Auto-linking React Native modules for target `yolo74`: RNGestureHandler, RNReanimated, RNScreens, and react-native-safe-area-context' + os.EOL +
60    'Analyzing dependencies' + os.EOL +
61    '[!] CocoaPods could not find compatible versions for pod "EXFileSystem":' + os.EOL +
62    '  In snapshot (Podfile.lock):' + os.EOL +
63    '    EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL +
64    '' + os.EOL +
65    '  In Podfile:' + os.EOL +
66    '    EXFileSystem (from `../node_modules/expo-file-system/ios`)' + os.EOL +
67    '' + os.EOL +
68    '' + os.EOL +
69    'You have either:' + os.EOL +
70    ' * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.' + os.EOL +
71    ' * changed the constraints of dependency `EXFileSystem` inside your development pod `EXFileSystem`.' + os.EOL +
72    "   You should run `pod update EXFileSystem` to apply changes you've made.\n",
73  // prettier-ignore
74  stderr:
75    'Ignoring ffi-1.13.1 because its extensions are not built. Try: gem pristine ffi --version 1.13.1' + os.EOL +
76    'Ignoring digest-crc-0.6.1 because its extensions are not built. Try: gem pristine digest-crc --version 0.6.1' + os.EOL +
77    'Ignoring ffi-1.13.1 because its extensions are not built. Try: gem pristine ffi --version 1.13.1' + os.EOL +
78    'Ignoring unf_ext-0.0.7.7 because its extensions are not built. Try: gem pristine unf_ext --version 0.0.7.7\n',
79  status: 1,
80  signal: null,
81};
82
83describe(getPodUpdateMessage, () => {
84  it(`matches pod update`, () => {
85    expect(getPodUpdateMessage(fakePodRepoUpdateErrorOutput.stdout)).toStrictEqual({
86      shouldUpdateRepo: true,
87      updatePackage: 'EXFileSystem',
88    });
89  });
90  it(`matches pod update without repo update`, () => {
91    expect(
92      getPodUpdateMessage(
93        "It seems like you've changed the version of the dependency `React-Core/RCTWebSocket` and it differs from the version stored in `Pods/Local Podspecs`.\n" +
94          'You should run `pod update React-Core/RCTWebSocket --no-repo-update` to apply changes made locally.\n'
95      )
96    ).toStrictEqual({
97      shouldUpdateRepo: false,
98      updatePackage: 'React-Core/RCTWebSocket',
99    });
100  });
101});
102
103describe(getPodRepoUpdateMessage, () => {
104  it(`formats pod repo update message`, () => {
105    expect(
106      stripAnsi(
107        getPodRepoUpdateMessage(
108          '[!] Unable to find a specification for `expo-dev-menu-interface` depended upon by `expo-dev-launcher`'
109        ).message
110      )
111    ).toBe(
112      `Couldn't install: expo-dev-launcher » expo-dev-menu-interface. Updating the Pods project and trying again...`
113    );
114  });
115  it(`formats pod update message`, () => {
116    expect(
117      stripAnsi(
118        getPodRepoUpdateMessage(
119          "You should run `pod update EXFileSystem` to apply changes you've made."
120        ).message
121      )
122    ).toBe(`Couldn't install: EXFileSystem. Updating the Pods project and trying again...`);
123  });
124});
125
126describe('installAsync', () => {
127  it(`does pod repo update automatically when the Podfile.lock is malformed`, async () => {
128    const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager');
129    const manager = new CocoaPodsPackageManager({ cwd: projectRoot });
130
131    manager._runAsync = jest.fn((commands: string[]) => {
132      const cmd = commands.join(' ');
133      if (['install', 'update EXFileSystem', 'install --repo-update'].includes(cmd)) {
134        throw fakePodRepoUpdateErrorOutput;
135      }
136      // eslint-disable-next-line no-throw-literal
137      throw 'unhandled ig';
138    });
139
140    await expect(manager.installAsync()).rejects.toThrowErrorMatchingInlineSnapshot(`
141      "Command \`pod install --repo-update\` failed.
142      └─ Cause: This is often due to native package versions mismatching. Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve.
143
144      [!] CocoaPods could not find compatible versions for pod "EXFileSystem":
145        In snapshot (Podfile.lock):
146          EXFileSystem (from \`../node_modules/expo-file-system/ios\`)
147      
148        In Podfile:
149          EXFileSystem (from \`../node_modules/expo-file-system/ios\`)
150      
151      
152      You have either:
153       * out-of-date source repos which you can update with \`pod repo update\` or with \`pod install --repo-update\`.
154       * changed the constraints of dependency \`EXFileSystem\` inside your development pod \`EXFileSystem\`.
155         You should run \`pod update EXFileSystem\` to apply changes you've made.
156      "
157    `);
158
159    // `pod install` > `pod update EXFileSystem` > `pod repo update` > `pod install`
160    expect(manager._runAsync).toHaveBeenNthCalledWith(1, ['install']);
161    expect(manager._runAsync).toHaveBeenNthCalledWith(2, ['update', 'EXFileSystem']);
162    expect(manager._runAsync).toHaveBeenNthCalledWith(3, ['install', '--repo-update']);
163    expect(manager._runAsync).toBeCalledTimes(3);
164  });
165
166  it(`auto updates malformed package versions`, async () => {
167    const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager');
168    const manager = new CocoaPodsPackageManager({ cwd: projectRoot });
169
170    let invokedOnce = false;
171    manager._runAsync = jest.fn((commands: string[]) => {
172      const cmd = commands.join(' ');
173      if (cmd === 'install') {
174        // On the second invocation, return a successful result.
175        if (invokedOnce) {
176          return {};
177        }
178        invokedOnce = true;
179        throw fakePodRepoUpdateErrorOutput;
180      }
181      if (cmd === 'update EXFileSystem') {
182        return {};
183      }
184      if (cmd === 'repo update') {
185        return {};
186      }
187      // eslint-disable-next-line no-throw-literal
188      throw 'unhandled ig';
189    });
190
191    // Ensure an error is not thrown
192    await manager.installAsync();
193
194    // `pod install` > `pod update EXFileSystem` > `pod install`
195    expect(manager._runAsync).toHaveBeenNthCalledWith(1, ['install']);
196    expect(manager._runAsync).toHaveBeenNthCalledWith(2, ['update', 'EXFileSystem']);
197    expect(manager._runAsync).toBeCalledTimes(2);
198  });
199
200  it(`runs install as expected`, async () => {
201    const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager');
202    const manager = new CocoaPodsPackageManager({ cwd: projectRoot });
203
204    manager._runAsync = jest.fn((commands: string[]) => {
205      const cmd = commands.join(' ');
206      if (cmd === 'install') {
207        return {};
208      }
209      // eslint-disable-next-line no-throw-literal
210      throw 'unhandled ig';
211    });
212
213    // Ensure an error is not thrown
214    await manager.installAsync();
215
216    // `pod install` > success
217    expect(manager._runAsync).toHaveBeenNthCalledWith(1, ['install']);
218    expect(manager._runAsync).toBeCalledTimes(1);
219  });
220});
221
222it(`throws for unimplemented methods`, async () => {
223  const manager = new CocoaPodsPackageManager({ cwd: projectRoot });
224
225  expect(() => manager.addAsync()).toThrow('Unimplemented');
226  expect(() => manager.addDevAsync()).toThrow('Unimplemented');
227  expect(() => manager.addGlobalAsync()).toThrow('Unimplemented');
228  expect(() => manager.removeAsync([])).toThrow('Unimplemented');
229  expect(() => manager.removeDevAsync([])).toThrow('Unimplemented');
230  expect(() => manager.removeGlobalAsync([])).toThrow('Unimplemented');
231  await expect(manager.configAsync('')).rejects.toThrow('Unimplemented');
232  await expect(manager.removeLockfileAsync()).rejects.toThrow('Unimplemented');
233  await expect(manager.uninstallAsync()).rejects.toThrow('Unimplemented');
234});
235
236it(`gets the cocoapods version`, async () => {
237  const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager');
238  const manager = new CocoaPodsPackageManager({ cwd: projectRoot });
239
240  mockedSpawnAsync.mockImplementation(() => mockSpawnPromise(Promise.resolve({ stdout: '1.9.1' })));
241
242  expect(await manager.versionAsync()).toBe('1.9.1');
243});
244
245it(`can detect if the CLI is installed`, async () => {
246  const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager');
247  const manager = new CocoaPodsPackageManager({ cwd: projectRoot });
248
249  mockedSpawnAsync.mockImplementation(() => mockSpawnPromise(Promise.resolve({ stdout: '1.9.1' })));
250
251  expect(await manager.isCLIInstalledAsync()).toBe(true);
252});
253
254it(`can get the directory of a pods project`, async () => {
255  const projectRoot = getRoot('cocoapods-detect-pods');
256  const iosRoot = path.join(projectRoot, 'ios');
257  await fs.ensureDir(iosRoot);
258
259  // first test when no pod project exists
260  const { CocoaPodsPackageManager } = require('../CocoaPodsPackageManager');
261  expect(CocoaPodsPackageManager.getPodProjectRoot(projectRoot)).toBe(null);
262
263  // next test the ios/ folder
264  fs.writeFileSync(path.join(iosRoot, 'Podfile'), '...');
265
266  expect(CocoaPodsPackageManager.getPodProjectRoot(projectRoot)).toBe(iosRoot);
267
268  // finally test that the current directory has higher priority than the ios directory
269  fs.writeFileSync(path.join(projectRoot, 'Podfile'), '...');
270  expect(CocoaPodsPackageManager.getPodProjectRoot(projectRoot)).toBe(projectRoot);
271});
272
273describe('isAvailable', () => {
274  let platform: string;
275  let originalLog: any;
276  beforeAll(() => {
277    platform = process.platform;
278    originalLog = console.log;
279  });
280  afterEach(() => {
281    Object.defineProperty(process, 'platform', {
282      value: platform,
283    });
284    console.log = originalLog;
285  });
286  it(`does not support non-darwin machines`, () => {
287    // set the platform to something other than darwin
288    Object.defineProperty(process, 'platform', {
289      value: 'something-random',
290    });
291    console.log = jest.fn();
292    expect(CocoaPodsPackageManager.isAvailable(projectRoot, false)).toBe(false);
293    expect(console.log).toBeCalledTimes(1);
294  });
295  it(`does not support projects without Podfiles`, async () => {
296    // ensure the platform is darwin
297    Object.defineProperty(process, 'platform', {
298      value: 'darwin',
299    });
300    // create a fake project without a Podfile
301    const projectRoot = getRoot('cocoapods-detect-available');
302    await fs.ensureDir(projectRoot);
303
304    let message = '';
305    console.log = jest.fn((msg) => (message = msg));
306
307    expect(CocoaPodsPackageManager.isAvailable(projectRoot, false)).toBe(false);
308    expect(console.log).toBeCalledTimes(1);
309    expect(message).toMatch(/not supported in this project/);
310  });
311});
312