1import { vol } from 'memfs'; 2 3import { asMock } from '../../../__tests__/asMock'; 4import { NgrokInstance, NgrokResolver } 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 isNgrokClientError: jest.requireActual('../../doctor/ngrok/NgrokResolver').isNgrokClientError, 23 NgrokResolver: jest.fn(() => ({ 24 resolveAsync: jest.fn(async () => instance), 25 get: jest.fn(async () => instance), 26 })), 27 }; 28}); 29jest.mock('../../platforms/android/adbReverse', () => ({ 30 startAdbReverseAsync: jest.fn(async () => true), 31})); 32jest.mock('../../../utils/exit'); 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 47const originalEnv = process.env; 48 49afterAll(() => { 50 process.env = originalEnv; 51}); 52 53beforeEach(() => { 54 vol.reset(); 55}); 56 57describe('getActiveUrl', () => { 58 it(`is loaded on start`, async () => { 59 const { ngrok } = createNgrokInstance(); 60 expect(ngrok.getActiveUrl()).toBeNull(); 61 await ngrok.startAsync(); 62 expect(ngrok.getActiveUrl()).toEqual('http://localhost:3000'); 63 }); 64}); 65 66describe('startAsync', () => { 67 beforeEach(() => { 68 delete process.env.EXPO_TUNNEL_SUBDOMAIN; 69 }); 70 it(`fails if adb reverse doesn't work`, async () => { 71 const { ngrok } = createNgrokInstance(); 72 asMock(startAdbReverseAsync).mockResolvedValueOnce(false); 73 74 await expect(ngrok.startAsync()).rejects.toThrow(/adb/); 75 }); 76 it(`starts`, async () => { 77 const { ngrok } = createNgrokInstance(); 78 expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000'); 79 }); 80 it(`starts with custom subdomain`, async () => { 81 process.env.EXPO_TUNNEL_SUBDOMAIN = 'test'; 82 const { ngrok } = createNgrokInstance(); 83 expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000'); 84 const instance = await new NgrokResolver('/').resolveAsync(); 85 expect(instance.connect).toBeCalledWith(expect.objectContaining({ subdomain: 'test' })); 86 }); 87 it(`starts with any subdomain`, async () => { 88 process.env.EXPO_TUNNEL_SUBDOMAIN = '1'; 89 const { ngrok } = createNgrokInstance(); 90 expect(await ngrok._connectToNgrokAsync()).toEqual('http://localhost:3000'); 91 const instance = await new NgrokResolver('/').resolveAsync(); 92 expect(instance.connect).toBeCalledWith( 93 expect.objectContaining({ subdomain: expect.stringMatching(/.*-anonymous-3000$/) }) 94 ); 95 }); 96 97 it(`retries three times`, async () => { 98 const { ngrok } = createNgrokInstance(); 99 100 // Add a connect which always fails. 101 const connect = jest.fn(() => { 102 throw new Error('woops'); 103 }); 104 ngrok.resolver.resolveAsync = jest.fn(async () => ({ connect } as any)); 105 106 await expect( 107 ngrok._connectToNgrokAsync({ 108 // Lower the time out to speed up the test. 109 timeout: 10, 110 }) 111 ).rejects.toThrow(/woops/); 112 // Runs the function three times. 113 expect(connect).toHaveBeenCalledTimes(3); 114 }); 115 it(`fixes invalid URL error by changing the randomness`, async () => { 116 const { ngrok, projectRoot } = createNgrokInstance(); 117 vol.fromJSON({}, projectRoot); 118 119 ngrok._resetProjectRandomnessAsync = jest.fn(ngrok._resetProjectRandomnessAsync.bind(ngrok)); 120 // Add a connect which throws an invalid URL error, then works the second time. 121 const connect = jest 122 .fn() 123 .mockImplementationOnce(() => { 124 const err = new Error(); 125 // @ts-expect-error 126 err.body = { msg: '...', error_code: 103 }; 127 128 throw err; 129 }) 130 .mockImplementationOnce(() => 'http://localhost:3000'); 131 ngrok.resolver.resolveAsync = jest.fn(async () => ({ connect } as any)); 132 133 await ngrok._connectToNgrokAsync(); 134 135 // Once for the initial generation and once for the retry. 136 expect(ngrok._resetProjectRandomnessAsync).toHaveBeenCalledTimes(2); 137 expect(connect).toHaveBeenCalledTimes(2); 138 }); 139}); 140 141describe('_getProjectHostnameAsync', () => { 142 it(`generates a valid hostname`, async () => { 143 const { projectRoot, ngrok } = createNgrokInstance(); 144 vol.fromJSON({}, projectRoot); 145 146 const hostname = await ngrok._getProjectHostnameAsync(); 147 expect(hostname).toEqual(expect.stringMatching(/.*\.anonymous\.3000\.exp\.direct/)); 148 149 // URL-safe 150 expect(encodeURIComponent(hostname)).toEqual(hostname); 151 152 // Works twice in a row... 153 expect(await ngrok._getProjectHostnameAsync()).toEqual( 154 expect.stringMatching(/.*\.anonymous\.3000\.exp\.direct/) 155 ); 156 157 // randomness is persisted 158 expect(JSON.parse(vol.toJSON()['/.expo/settings.json']).urlRandomness).toBeDefined(); 159 }); 160}); 161