import { renderHook } from '@testing-library/react-hooks'; import type { EnvironmentWithOptionalUrl } from './use-config'; import { useConfig, LOCAL_STORAGE_NETWORK_KEY } from './use-config'; import { Networks } from '../types'; type HostMapping = Record; const mockHostsMap: HostMapping = { 'https://host1.com': 300, 'https://host2.com': 500, 'https://host3.com': 100, 'https://host4.com': 650, }; const hostList = Object.keys(mockHostsMap); const mockEnvironment: EnvironmentWithOptionalUrl = { VEGA_ENV: Networks.TESTNET, VEGA_CONFIG_URL: 'https://vega.url/config.json', VEGA_NETWORKS: {}, ETHEREUM_PROVIDER_URL: 'https://ethereum.provider', ETHERSCAN_URL: 'https://etherscan.url', GIT_BRANCH: 'test', GIT_ORIGIN_URL: 'https://github.com/test/repo', GIT_COMMIT_HASH: 'abcde01234', GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback', }; function setupFetch(configUrl: string, hostMap: HostMapping) { const hostUrls = Object.keys(hostMap); return (url: RequestInfo) => { if (url === configUrl) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ hosts: hostUrls }), } as Response); } if (hostUrls.includes(url as string)) { const value = hostMap[url as string]; return new Promise((resolve, reject) => { if (typeof value === 'number') { setTimeout(() => { resolve({ ok: true } as Response); }, value); } else { reject(value); } }); } return Promise.resolve({ ok: true, } as Response); }; } // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; global.fetch = jest.fn(); const mockUpdate = jest.fn(); beforeEach(() => { jest.useFakeTimers(); mockUpdate.mockClear(); window.localStorage.clear(); // @ts-ignore typescript doesn't recognise the mocked instance global.fetch.mockReset(); // @ts-ignore typescript doesn't recognise the mocked instance global.fetch.mockImplementation( setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMap) ); }); afterAll(() => { // @ts-ignore: typescript doesn't recognise the mocked fetch instance fetch.mockRestore(); }); describe('useConfig hook', () => { it('updates the environment with a host url from the network configuration', async () => { const allowedStatuses = [ 'idle', 'loading-config', 'loading-node', 'success', ]; const { result, waitForNextUpdate } = renderHook(() => useConfig(mockEnvironment, mockUpdate) ); await waitForNextUpdate(); jest.runAllTimers(); await waitForNextUpdate(); expect(result.current.status).toBe('success'); result.all.forEach((state) => { expect(allowedStatuses).toContain('status' in state && state.status); }); // fetches config expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); // calls each node hostList.forEach((url) => { expect(fetch).toHaveBeenCalledWith(url); }); // updates the environment expect(hostList).toContain(mockUpdate.mock.calls[0][0]({}).VEGA_URL); }); it('uses the host from the configuration which responds first', async () => { const shortestResponseTime = Object.values(mockHostsMap).sort()[0]; const expectedHost = hostList.find((url: keyof typeof mockHostsMap) => { return mockHostsMap[url] === shortestResponseTime; }); const { result, waitForNextUpdate } = renderHook(() => useConfig(mockEnvironment, mockUpdate) ); await waitForNextUpdate(); jest.runAllTimers(); await waitForNextUpdate(); expect(result.current.status).toBe('success'); expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe(expectedHost); }); it('ignores failing hosts and uses one which returns a success response', async () => { const mockHostsMapScoped = { 'https://host1.com': 350, 'https://host2.com': new Error('Server error'), 'https://host3.com': 230, 'https://host4.com': new Error('Server error'), }; // @ts-ignore typescript doesn't recognise the mocked instance global.fetch.mockImplementation( setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped) ); const { result, waitForNextUpdate } = renderHook(() => useConfig(mockEnvironment, mockUpdate) ); await waitForNextUpdate(); jest.runAllTimers(); await waitForNextUpdate(); expect(result.current.status).toBe('success'); expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe('https://host3.com'); }); it('returns the correct error status for when the config cannot be accessed', async () => { // @ts-ignore typescript doesn't recognise the mocked instance global.fetch.mockImplementation((url: RequestInfo) => { if (url === mockEnvironment.VEGA_CONFIG_URL) { return Promise.reject(new Error('Server error')); } return Promise.resolve({ ok: true } as Response); }); const { result, waitForNextUpdate } = renderHook(() => useConfig(mockEnvironment, mockUpdate) ); await waitForNextUpdate(); expect(result.current.status).toBe('error-loading-config'); expect(mockUpdate).not.toHaveBeenCalled(); }); it('returns the correct error status for when the config is not valid', async () => { // @ts-ignore typescript doesn't recognise the mocked instance global.fetch.mockImplementation((url: RequestInfo) => { if (url === mockEnvironment.VEGA_CONFIG_URL) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ some: 'data' }), }); } return Promise.resolve({ ok: true } as Response); }); const { result, waitForNextUpdate } = renderHook(() => useConfig(mockEnvironment, mockUpdate) ); await waitForNextUpdate(); expect(result.current.status).toBe('error-validating-config'); expect(mockUpdate).not.toHaveBeenCalled(); }); it('returns the correct error status for when no hosts can be accessed', async () => { const mockHostsMapScoped = { 'https://host1.com': new Error('Server error'), 'https://host2.com': new Error('Server error'), 'https://host3.com': new Error('Server error'), 'https://host4.com': new Error('Server error'), }; // @ts-ignore typescript doesn't recognise the mocked instance global.fetch.mockImplementation( setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped) ); const { result, waitForNextUpdate } = renderHook(() => useConfig(mockEnvironment, mockUpdate) ); await waitForNextUpdate(); expect(result.current.status).toBe('error-loading-node'); expect(mockUpdate).not.toHaveBeenCalled(); }); it('caches the list of networks', async () => { const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); await run1.waitForNextUpdate(); jest.runAllTimers(); await run1.waitForNextUpdate(); expect(run1.result.current.status).toBe('success'); expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); // @ts-ignore typescript doesn't recognise the mocked instance fetch.mockClear(); const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); jest.runAllTimers(); await run2.waitForNextUpdate(); expect(run2.result.current.status).toBe('success'); expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); }); it('caches the list of networks between runs', async () => { const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); await run1.waitForNextUpdate(); jest.runAllTimers(); await run1.waitForNextUpdate(); expect(run1.result.current.status).toBe('success'); expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); // @ts-ignore typescript doesn't recognise the mocked instance fetch.mockClear(); const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); jest.runAllTimers(); await run2.waitForNextUpdate(); expect(run2.result.current.status).toBe('success'); expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); }); it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => { window.localStorage.setItem( `${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`, '{not:{valid:{json' ); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop); const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); await run1.waitForNextUpdate(); jest.runAllTimers(); await run1.waitForNextUpdate(); expect(run1.result.current.status).toBe('success'); expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); expect(consoleWarnSpy).toHaveBeenCalled(); consoleWarnSpy.mockRestore(); }); it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => { window.localStorage.setItem( `${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`, JSON.stringify({ invalid: 'data' }) ); const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop); const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); await run1.waitForNextUpdate(); jest.runAllTimers(); await run1.waitForNextUpdate(); expect(run1.result.current.status).toBe('success'); expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); });