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