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