18d307f52SEvan Baconimport { vol } from 'memfs';
28d307f52SEvan Bacon
3fa5bc561SWill Schurmanimport { asMock } from '../../../__tests__/asMock';
45723d92eSEvan Baconimport { NgrokInstance, NgrokResolver } from '../../doctor/ngrok/NgrokResolver';
50b294b94SEvan Baconimport { hasAdbReverseAsync, startAdbReverseAsync } from '../../platforms/android/adbReverse';
68d307f52SEvan Baconimport { AsyncNgrok } from '../AsyncNgrok';
78d307f52SEvan Bacon
84c50faceSEvan Baconjest.mock('../../../log');
98d307f52SEvan Baconjest.mock('../../../utils/delay', () => ({
108d307f52SEvan Bacon  delayAsync: jest.fn(async () => {}),
118d307f52SEvan Bacon  resolveWithTimeout: jest.fn(async (fn) => fn()),
128d307f52SEvan Bacon}));
138d307f52SEvan Baconjest.mock('../../../api/settings');
148d307f52SEvan Baconjest.mock('../../doctor/ngrok/NgrokResolver', () => {
158d307f52SEvan Bacon  const instance: NgrokInstance = {
168d307f52SEvan Bacon    getActiveProcess: jest.fn(),
178d307f52SEvan Bacon    connect: jest.fn(async () => 'http://localhost:3000'),
188d307f52SEvan Bacon    kill: jest.fn(),
198d307f52SEvan Bacon  };
208d307f52SEvan Bacon
218d307f52SEvan Bacon  return {
225723d92eSEvan Bacon    isNgrokClientError: jest.requireActual('../../doctor/ngrok/NgrokResolver').isNgrokClientError,
238d307f52SEvan Bacon    NgrokResolver: jest.fn(() => ({
248d307f52SEvan Bacon      resolveAsync: jest.fn(async () => instance),
258d307f52SEvan Bacon      get: jest.fn(async () => instance),
268d307f52SEvan Bacon    })),
278d307f52SEvan Bacon  };
288d307f52SEvan Bacon});
298d307f52SEvan Baconjest.mock('../../platforms/android/adbReverse', () => ({
300b294b94SEvan Bacon  hasAdbReverseAsync: jest.fn(async () => true),
318d307f52SEvan Bacon  startAdbReverseAsync: jest.fn(async () => true),
328d307f52SEvan Bacon}));
338d307f52SEvan Baconjest.mock('../../../utils/exit');
348d307f52SEvan Bacon
358d307f52SEvan Baconfunction createNgrokInstance() {
368d307f52SEvan Bacon  const projectRoot = '/';
378d307f52SEvan Bacon  const port = 3000;
388d307f52SEvan Bacon  const ngrok = new AsyncNgrok(projectRoot, port);
398d307f52SEvan Bacon  ngrok.getActiveUrl = jest.fn(ngrok.getActiveUrl.bind(ngrok));
408d307f52SEvan Bacon  ngrok.stopAsync = jest.fn(ngrok.stopAsync.bind(ngrok));
418d307f52SEvan Bacon  return {
428d307f52SEvan Bacon    projectRoot,
438d307f52SEvan Bacon    port,
448d307f52SEvan Bacon    ngrok,
458d307f52SEvan Bacon  };
468d307f52SEvan Bacon}
478d307f52SEvan Bacon
485723d92eSEvan Baconconst originalEnv = process.env;
495723d92eSEvan Bacon
505723d92eSEvan BaconafterAll(() => {
515723d92eSEvan Bacon  process.env = originalEnv;
525723d92eSEvan Bacon});
535723d92eSEvan Bacon
548d307f52SEvan BaconbeforeEach(() => {
558d307f52SEvan Bacon  vol.reset();
568d307f52SEvan Bacon});
578d307f52SEvan Bacon
588d307f52SEvan Bacondescribe('getActiveUrl', () => {
598d307f52SEvan Bacon  it(`is loaded on start`, async () => {
608d307f52SEvan Bacon    const { ngrok } = createNgrokInstance();
618d307f52SEvan Bacon    expect(ngrok.getActiveUrl()).toBeNull();
628d307f52SEvan Bacon    await ngrok.startAsync();
638d307f52SEvan Bacon    expect(ngrok.getActiveUrl()).toEqual('http://localhost:3000');
648d307f52SEvan Bacon  });
658d307f52SEvan Bacon});
668d307f52SEvan Bacon
678d307f52SEvan Bacondescribe('startAsync', () => {
680b294b94SEvan Bacon  it(`skips adb reverse if Android cannot be found`, async () => {
690b294b94SEvan Bacon    const { ngrok } = createNgrokInstance();
700b294b94SEvan Bacon    asMock(hasAdbReverseAsync).mockReturnValueOnce(false);
710b294b94SEvan Bacon
720b294b94SEvan Bacon    await ngrok.startAsync();
730b294b94SEvan Bacon    expect(startAdbReverseAsync).not.toBeCalled();
740b294b94SEvan Bacon  });
755723d92eSEvan Bacon  beforeEach(() => {
765723d92eSEvan Bacon    delete process.env.EXPO_TUNNEL_SUBDOMAIN;
775723d92eSEvan Bacon  });
788d307f52SEvan Bacon  it(`fails if adb reverse doesn't work`, async () => {
798d307f52SEvan Bacon    const { ngrok } = createNgrokInstance();
808d307f52SEvan Bacon    asMock(startAdbReverseAsync).mockResolvedValueOnce(false);
818d307f52SEvan Bacon
828d307f52SEvan Bacon    await expect(ngrok.startAsync()).rejects.toThrow(/adb/);
838d307f52SEvan Bacon  });
848d307f52SEvan Bacon  it(`starts`, async () => {
858d307f52SEvan Bacon    const { ngrok } = createNgrokInstance();
868d307f52SEvan Bacon    expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000');
878d307f52SEvan Bacon  });
885723d92eSEvan Bacon  it(`starts with custom subdomain`, async () => {
895723d92eSEvan Bacon    process.env.EXPO_TUNNEL_SUBDOMAIN = 'test';
905723d92eSEvan Bacon    const { ngrok } = createNgrokInstance();
915723d92eSEvan Bacon    expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000');
925723d92eSEvan Bacon    const instance = await new NgrokResolver('/').resolveAsync();
935723d92eSEvan Bacon    expect(instance.connect).toBeCalledWith(expect.objectContaining({ subdomain: 'test' }));
945723d92eSEvan Bacon  });
955723d92eSEvan Bacon  it(`starts with any subdomain`, async () => {
965723d92eSEvan Bacon    process.env.EXPO_TUNNEL_SUBDOMAIN = '1';
975723d92eSEvan Bacon    const { ngrok } = createNgrokInstance();
985723d92eSEvan Bacon    expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000');
995723d92eSEvan Bacon    const instance = await new NgrokResolver('/').resolveAsync();
1005723d92eSEvan Bacon    expect(instance.connect).toBeCalledWith(
1015723d92eSEvan Bacon      expect.objectContaining({ subdomain: expect.stringMatching(/.*-anonymous-3000$/) })
1025723d92eSEvan Bacon    );
1035723d92eSEvan Bacon  });
1048d307f52SEvan Bacon
1058d307f52SEvan Bacon  it(`retries three times`, async () => {
1068d307f52SEvan Bacon    const { ngrok } = createNgrokInstance();
1078d307f52SEvan Bacon
1088d307f52SEvan Bacon    // Add a connect which always fails.
1098d307f52SEvan Bacon    const connect = jest.fn(() => {
1108d307f52SEvan Bacon      throw new Error('woops');
1118d307f52SEvan Bacon    });
112*8a424bebSJames Ide    ngrok.resolver.resolveAsync = jest.fn(async () => ({ connect }) as any);
1138d307f52SEvan Bacon
1148d307f52SEvan Bacon    await expect(
1158d307f52SEvan Bacon      ngrok._connectToNgrokAsync({
1168d307f52SEvan Bacon        // Lower the time out to speed up the test.
1178d307f52SEvan Bacon        timeout: 10,
1188d307f52SEvan Bacon      })
1198d307f52SEvan Bacon    ).rejects.toThrow(/woops/);
1208d307f52SEvan Bacon    // Runs the function three times.
1218d307f52SEvan Bacon    expect(connect).toHaveBeenCalledTimes(3);
1228d307f52SEvan Bacon  });
1238d307f52SEvan Bacon  it(`fixes invalid URL error by changing the randomness`, async () => {
1248d307f52SEvan Bacon    const { ngrok, projectRoot } = createNgrokInstance();
1258d307f52SEvan Bacon    vol.fromJSON({}, projectRoot);
1268d307f52SEvan Bacon
1278d307f52SEvan Bacon    ngrok._resetProjectRandomnessAsync = jest.fn(ngrok._resetProjectRandomnessAsync.bind(ngrok));
1288d307f52SEvan Bacon    // Add a connect which throws an invalid URL error, then works the second time.
1298d307f52SEvan Bacon    const connect = jest
1308d307f52SEvan Bacon      .fn()
1318d307f52SEvan Bacon      .mockImplementationOnce(() => {
1328d307f52SEvan Bacon        const err = new Error();
1338d307f52SEvan Bacon        // @ts-expect-error
1345723d92eSEvan Bacon        err.body = { msg: '...', error_code: 103 };
1358d307f52SEvan Bacon
1368d307f52SEvan Bacon        throw err;
1378d307f52SEvan Bacon      })
1388d307f52SEvan Bacon      .mockImplementationOnce(() => 'http://localhost:3000');
139*8a424bebSJames Ide    ngrok.resolver.resolveAsync = jest.fn(async () => ({ connect }) as any);
1408d307f52SEvan Bacon
1418d307f52SEvan Bacon    await ngrok._connectToNgrokAsync();
1428d307f52SEvan Bacon
1438d307f52SEvan Bacon    // Once for the initial generation and once for the retry.
1448d307f52SEvan Bacon    expect(ngrok._resetProjectRandomnessAsync).toHaveBeenCalledTimes(2);
1458d307f52SEvan Bacon    expect(connect).toHaveBeenCalledTimes(2);
1468d307f52SEvan Bacon  });
1478d307f52SEvan Bacon});
1488d307f52SEvan Bacon
1498d307f52SEvan Bacondescribe('_getProjectHostnameAsync', () => {
1508d307f52SEvan Bacon  it(`generates a valid hostname`, async () => {
1518d307f52SEvan Bacon    const { projectRoot, ngrok } = createNgrokInstance();
1528d307f52SEvan Bacon    vol.fromJSON({}, projectRoot);
1538d307f52SEvan Bacon
1548d307f52SEvan Bacon    const hostname = await ngrok._getProjectHostnameAsync();
1558d307f52SEvan Bacon    expect(hostname).toEqual(expect.stringMatching(/.*\.anonymous\.3000\.exp\.direct/));
1568d307f52SEvan Bacon
1578d307f52SEvan Bacon    // URL-safe
1588d307f52SEvan Bacon    expect(encodeURIComponent(hostname)).toEqual(hostname);
1598d307f52SEvan Bacon
1608d307f52SEvan Bacon    // Works twice in a row...
1618d307f52SEvan Bacon    expect(await ngrok._getProjectHostnameAsync()).toEqual(
1628d307f52SEvan Bacon      expect.stringMatching(/.*\.anonymous\.3000\.exp\.direct/)
1638d307f52SEvan Bacon    );
1648d307f52SEvan Bacon
1658d307f52SEvan Bacon    // randomness is persisted
1668d307f52SEvan Bacon    expect(JSON.parse(vol.toJSON()['/.expo/settings.json']).urlRandomness).toBeDefined();
1678d307f52SEvan Bacon  });
1688d307f52SEvan Bacon});
169