1import { vol } from 'memfs';
2
3import { NgrokInstance } from '../../doctor/ngrok/NgrokResolver';
4import { startAdbReverseAsync } from '../../platforms/android/adbReverse';
5import { AsyncNgrok } from '../AsyncNgrok';
6
7jest.mock('../../../utils/delay', () => ({
8  delayAsync: jest.fn(async () => {}),
9  resolveWithTimeout: jest.fn(async (fn) => fn()),
10}));
11jest.mock('../../../api/settings');
12jest.mock('../../doctor/ngrok/NgrokResolver', () => {
13  const instance: NgrokInstance = {
14    getActiveProcess: jest.fn(),
15    connect: jest.fn(async () => 'http://localhost:3000'),
16    kill: jest.fn(),
17  };
18
19  return {
20    NgrokResolver: jest.fn(() => ({
21      resolveAsync: jest.fn(async () => instance),
22      get: jest.fn(async () => instance),
23    })),
24  };
25});
26jest.mock('../../platforms/android/adbReverse', () => ({
27  startAdbReverseAsync: jest.fn(async () => true),
28}));
29jest.mock('../../../utils/exit');
30
31const asMock = <T extends (...args: any[]) => any>(fn: T): jest.MockedFunction<T> =>
32  fn as jest.MockedFunction<T>;
33
34function createNgrokInstance() {
35  const projectRoot = '/';
36  const port = 3000;
37  const ngrok = new AsyncNgrok(projectRoot, port);
38  ngrok.getActiveUrl = jest.fn(ngrok.getActiveUrl.bind(ngrok));
39  ngrok.stopAsync = jest.fn(ngrok.stopAsync.bind(ngrok));
40  return {
41    projectRoot,
42    port,
43    ngrok,
44  };
45}
46
47beforeEach(() => {
48  vol.reset();
49});
50
51describe('getActiveUrl', () => {
52  it(`is loaded on start`, async () => {
53    const { ngrok } = createNgrokInstance();
54    expect(ngrok.getActiveUrl()).toBeNull();
55    await ngrok.startAsync();
56    expect(ngrok.getActiveUrl()).toEqual('http://localhost:3000');
57  });
58});
59
60describe('startAsync', () => {
61  it(`fails if adb reverse doesn't work`, async () => {
62    const { ngrok } = createNgrokInstance();
63    asMock(startAdbReverseAsync).mockResolvedValueOnce(false);
64
65    await expect(ngrok.startAsync()).rejects.toThrow(/adb/);
66  });
67  it(`starts`, async () => {
68    const { ngrok } = createNgrokInstance();
69    expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000');
70  });
71
72  it(`retries three times`, async () => {
73    const { ngrok } = createNgrokInstance();
74
75    // Add a connect which always fails.
76    const connect = jest.fn(() => {
77      throw new Error('woops');
78    });
79    ngrok.resolver.resolveAsync = jest.fn(async () => ({ connect } as any));
80
81    await expect(
82      ngrok._connectToNgrokAsync({
83        // Lower the time out to speed up the test.
84        timeout: 10,
85      })
86    ).rejects.toThrow(/woops/);
87    // Runs the function three times.
88    expect(connect).toHaveBeenCalledTimes(3);
89  });
90  it(`fixes invalid URL error by changing the randomness`, async () => {
91    const { ngrok, projectRoot } = createNgrokInstance();
92    vol.fromJSON({}, projectRoot);
93
94    ngrok._resetProjectRandomnessAsync = jest.fn(ngrok._resetProjectRandomnessAsync.bind(ngrok));
95    // Add a connect which throws an invalid URL error, then works the second time.
96    const connect = jest
97      .fn()
98      .mockImplementationOnce(() => {
99        const err = new Error();
100        // @ts-expect-error
101        err.error_code = 103;
102
103        throw err;
104      })
105      .mockImplementationOnce(() => 'http://localhost:3000');
106    ngrok.resolver.resolveAsync = jest.fn(async () => ({ connect } as any));
107
108    await ngrok._connectToNgrokAsync();
109
110    // Once for the initial generation and once for the retry.
111    expect(ngrok._resetProjectRandomnessAsync).toHaveBeenCalledTimes(2);
112    expect(connect).toHaveBeenCalledTimes(2);
113  });
114});
115
116describe('_getProjectHostnameAsync', () => {
117  it(`generates a valid hostname`, async () => {
118    const { projectRoot, ngrok } = createNgrokInstance();
119    vol.fromJSON({}, projectRoot);
120
121    const hostname = await ngrok._getProjectHostnameAsync();
122    expect(hostname).toEqual(expect.stringMatching(/.*\.anonymous\.3000\.exp\.direct/));
123
124    // URL-safe
125    expect(encodeURIComponent(hostname)).toEqual(hostname);
126
127    // Works twice in a row...
128    expect(await ngrok._getProjectHostnameAsync()).toEqual(
129      expect.stringMatching(/.*\.anonymous\.3000\.exp\.direct/)
130    );
131
132    // randomness is persisted
133    expect(JSON.parse(vol.toJSON()['/.expo/settings.json']).urlRandomness).toBeDefined();
134  });
135});
136