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