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