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