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