UNPKG

@openzeppelin/contracts-ui-builder-adapter-stellar

Version:
358 lines (297 loc) 12.7 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { StellarNetworkConfig } from '@openzeppelin/contracts-ui-builder-types'; import { appConfigService } from '@openzeppelin/contracts-ui-builder-utils'; import { resolveRpcUrl, testStellarRpcConnection, validateStellarRpcEndpoint } from '../rpc'; // Helper to create a mock StellarNetworkConfig const createMockConfig = ( id: string, sorobanRpcUrl?: string, name?: string ): StellarNetworkConfig => ({ id, name: name || `Test ${id}`, ecosystem: 'stellar', network: 'stellar', type: 'testnet', isTestnet: true, exportConstName: id.replace(/-/g, ''), horizonUrl: `https://horizon-${id}.stellar.org`, sorobanRpcUrl: sorobanRpcUrl || '', // Allow undefined or empty for testing error cases networkPassphrase: `Test ${id} Network`, explorerUrl: `https://stellar.expert/explorer/${id}`, icon: 'stellar', }); // Mock the appConfigService from the correct package vi.mock('@openzeppelin/contracts-ui-builder-utils', async (importOriginal) => { const original = await importOriginal<typeof import('@openzeppelin/contracts-ui-builder-utils')>(); return { ...original, logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, appConfigService: { getRpcEndpointOverride: vi.fn(), getConfig: vi.fn().mockReturnValue({ rpcEndpoints: {} }), }, }; }); describe('resolveRpcUrl', () => { beforeEach(() => { // Reset mocks before each test to ensure test isolation vi.mocked(appConfigService.getRpcEndpointOverride).mockReset(); vi.mocked(appConfigService.getConfig).mockReturnValue({ rpcEndpoints: {} }); }); it('should use RPC override from AppConfigService if available (string)', () => { const networkId = 'stellar-testnet-override'; const overrideRpcUrl = 'https://custom-soroban-rpc.example.com'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(overrideRpcUrl); const config = createMockConfig(networkId, 'https://soroban-testnet.stellar.org'); expect(resolveRpcUrl(config)).toBe(overrideRpcUrl); }); it('should use RPC override from AppConfigService if available (object with http)', () => { const networkId = 'stellar-testnet-object-override'; const overrideRpcUrl = 'https://custom-soroban-object.example.com'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue({ http: overrideRpcUrl }); const config = createMockConfig(networkId, 'https://soroban-testnet.stellar.org'); expect(resolveRpcUrl(config)).toBe(overrideRpcUrl); }); it('should fall back to networkConfig.sorobanRpcUrl if no override is available', () => { const networkId = 'stellar-testnet-fallback'; const defaultRpcUrl = 'https://soroban-testnet.stellar.org'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined); const config = createMockConfig(networkId, defaultRpcUrl); expect(resolveRpcUrl(config)).toBe(defaultRpcUrl); }); it('should fall back to networkConfig.sorobanRpcUrl if override is invalid URL', () => { const networkId = 'stellar-testnet-invalid-override'; const defaultRpcUrl = 'https://soroban-testnet.stellar.org'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue('invalid-url'); const config = createMockConfig(networkId, defaultRpcUrl); expect(resolveRpcUrl(config)).toBe(defaultRpcUrl); }); it('should throw an error if no valid Soroban RPC URL (override or default) is found', () => { const networkId = 'no-valid-soroban-rpc'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined); const config = createMockConfig(networkId, undefined); expect(() => resolveRpcUrl(config)).toThrowError( `No valid Soroban RPC URL configured for network Test ${networkId} (ID: ${networkId}).` ); }); it('should throw an error if default Soroban RPC is invalid and no override is found', () => { const networkId = 'invalid-default-soroban-rpc'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined); const config = createMockConfig(networkId, 'not-a-url'); expect(() => resolveRpcUrl(config)).toThrowError( `No valid Soroban RPC URL configured for network Test ${networkId} (ID: ${networkId}).` ); }); it('should use override even if default is invalid', () => { const networkId = 'override-wins-over-invalid-default'; const overrideRpcUrl = 'https://good-soroban-override.example.com'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(overrideRpcUrl); const config = createMockConfig(networkId, 'bad-default-url'); expect(resolveRpcUrl(config)).toBe(overrideRpcUrl); }); it('should work with mainnet Soroban RPC URL', () => { const networkId = 'stellar-mainnet'; const mainnetRpcUrl = 'https://mainnet.sorobanrpc.com'; vi.mocked(appConfigService.getRpcEndpointOverride).mockReturnValue(undefined); const config = createMockConfig(networkId, mainnetRpcUrl); expect(resolveRpcUrl(config)).toBe(mainnetRpcUrl); }); }); describe('validateStellarRpcEndpoint', () => { it('should return true for valid Soroban RPC URLs', () => { const validConfigs = [ { url: 'https://soroban-testnet.stellar.org', name: 'Official Testnet', isCustom: false }, { url: 'https://mainnet.sorobanrpc.com', name: 'Official Mainnet', isCustom: false }, { url: 'https://stellar.publicnode.com', name: 'Public Node', isCustom: false }, { url: 'https://custom-soroban.example.com', name: 'Custom', isCustom: true }, ]; validConfigs.forEach((config) => { expect(validateStellarRpcEndpoint(config)).toBe(true); }); }); it('should return false for invalid URLs', () => { const invalidConfigs = [ { url: 'not-a-url', name: 'Invalid', isCustom: false }, { url: '', name: 'Empty URL', isCustom: false }, ]; invalidConfigs.forEach((config) => { expect(validateStellarRpcEndpoint(config)).toBe(false); }); }); it('should return true for HTTP localhost URLs (development)', () => { const devConfig = { url: 'http://localhost:8000/soroban/rpc', name: 'Local Dev', isCustom: true, }; expect(validateStellarRpcEndpoint(devConfig)).toBe(true); }); it('should handle validation errors gracefully', () => { // This test verifies the catch block works, but doMock doesn't work in the middle of test execution // Instead, we'll test that the function returns false on invalid input const config = { url: null as unknown as string, name: 'Test', isCustom: false }; expect(validateStellarRpcEndpoint(config)).toBe(false); }); }); describe('testStellarRpcConnection', () => { beforeEach(() => { // Reset fetch mock before each test global.fetch = vi.fn(); vi.clearAllTimers(); vi.useFakeTimers(); // Mock Date.now to simulate time passage const mockDate = new Date('2023-01-01T00:00:00Z'); vi.setSystemTime(mockDate); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); it('should return success for healthy Soroban RPC endpoint', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ jsonrpc: '2.0', id: 1, result: { status: 'healthy' }, }), }; // Mock fetch to simulate some latency vi.mocked(global.fetch).mockImplementation(async () => { // Advance time to simulate network latency vi.advanceTimersByTime(100); return mockResponse as unknown as Response; }); const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(true); expect(result.latency).toBeGreaterThan(0); expect(result.error).toBeUndefined(); }); it('should return success for valid getLatestLedger response (fallback)', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ jsonrpc: '2.0', id: 1, result: { sequence: 12345, hash: 'abc123' }, }), }; // Mock fetch to simulate some latency vi.mocked(global.fetch).mockImplementation(async () => { // Advance time to simulate network latency vi.advanceTimersByTime(150); return mockResponse as unknown as Response; }); const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(true); expect(result.latency).toBeGreaterThan(0); }); it('should return failure for HTTP error responses', async () => { const mockResponse = { ok: false, status: 500, }; vi.mocked(global.fetch).mockResolvedValue(mockResponse as unknown as Response); const config = { url: 'https://bad-soroban.example.com', name: 'Bad RPC', isCustom: true }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(false); expect(result.error).toBe('HTTP error: 500'); }); it('should return failure for JSON-RPC error responses', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ jsonrpc: '2.0', id: 1, error: { code: -32601, message: 'Method not found' }, }), }; vi.mocked(global.fetch).mockResolvedValue(mockResponse as unknown as Response); const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(false); expect(result.error).toContain('Method not found'); }); it('should return failure for unhealthy service status', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ jsonrpc: '2.0', id: 1, result: { status: 'unhealthy' }, }), }; // Mock fetch to simulate some latency vi.mocked(global.fetch).mockImplementation(async () => { // Advance time to simulate network latency vi.advanceTimersByTime(120); return mockResponse as unknown as Response; }); const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(false); expect(result.error).toContain('unhealthy'); expect(result.latency).toBeGreaterThan(0); }); it('should handle connection timeout', async () => { // Create a mock that immediately rejects with AbortError vi.mocked(global.fetch).mockRejectedValue( (() => { const error = new Error('The operation was aborted'); error.name = 'AbortError'; return error; })() ); const config = { url: 'https://slow-soroban.example.com', name: 'Slow RPC', isCustom: true }; const result = await testStellarRpcConnection(config, 1000); expect(result.success).toBe(false); expect(result.error).toContain('timeout'); }); it('should return failure for missing URL', async () => { const config = { url: '', name: 'Empty URL', isCustom: false }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(false); expect(result.error).toBe('Soroban RPC URL is required'); }); it('should handle network errors gracefully', async () => { vi.mocked(global.fetch).mockRejectedValue(new Error('Network error')); const config = { url: 'https://unreachable-soroban.example.com', name: 'Unreachable', isCustom: true, }; const result = await testStellarRpcConnection(config); expect(result.success).toBe(false); expect(result.error).toBe('Network error'); }); it('should use custom timeout value', async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ jsonrpc: '2.0', id: 1, result: { status: 'healthy' }, }), }; vi.mocked(global.fetch).mockResolvedValue(mockResponse as unknown as Response); const config = { url: 'https://soroban-testnet.stellar.org', name: 'Testnet', isCustom: false }; const result = await testStellarRpcConnection(config, 10000); // 10 second timeout expect(result.success).toBe(true); // Verify fetch was called with the signal expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ signal: expect.any(AbortSignal), }) ); }); });