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