From eb6b55cfbd96cc2b60bcf63591eebed3e95ca27a Mon Sep 17 00:00:00 2001 From: Timothy Egwuda Date: Sat, 25 Apr 2026 15:16:36 +0100 Subject: [PATCH] feat: Add focused tests for critical user scenarios - ErrorBoundary: Network error recovery and retry functionality - Header: Wallet connection failures and mobile menu handling - UserRiskProfile: Risk score interpretation and tier selection safety - No auto-generated files or coverage reports - Only meaningful source code changes --- package.json | 5 +- scripts/test-all-modules.js | 337 ------------------ scripts/test-api-retry.js | 191 ---------- scripts/test-env-validation-simple.js | 224 ------------ scripts/test-validation.js | 165 --------- scripts/validate-env.js | 261 -------------- .../__tests__/ErrorBoundary.test.jsx | 52 +++ src/components/__tests__/Header.test.jsx | 72 ++++ .../__tests__/UserRiskProfile.test.jsx | 55 +++ 9 files changed, 183 insertions(+), 1179 deletions(-) delete mode 100644 scripts/test-all-modules.js delete mode 100644 scripts/test-api-retry.js delete mode 100644 scripts/test-env-validation-simple.js delete mode 100644 scripts/test-validation.js delete mode 100644 scripts/validate-env.js create mode 100644 src/components/__tests__/ErrorBoundary.test.jsx create mode 100644 src/components/__tests__/Header.test.jsx create mode 100644 src/components/__tests__/UserRiskProfile.test.jsx diff --git a/package.json b/package.json index c08aa013..ac1cfaa4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "dependencies": { "@blend-capital/blend-sdk": "^2.2.0", diff --git a/scripts/test-all-modules.js b/scripts/test-all-modules.js deleted file mode 100644 index cfcc37b0..00000000 --- a/scripts/test-all-modules.js +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env node -/** - * Comprehensive test suite for all new modules - * Tests all 8+ issues resolved - */ - -const fs = require('fs'); -const path = require('path'); - -const colors = { - reset: '\x1b[0m', - green: '\x1b[32m', - red: '\x1b[31m', - cyan: '\x1b[36m', - blue: '\x1b[34m', - yellow: '\x1b[33m', -}; - -function log(color, message) { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -console.log(`\n${colors.cyan}${'='.repeat(70)}`); -console.log(`๐Ÿงช COMPREHENSIVE MODULE TEST SUITE`); -console.log(`${'='.repeat(70)}${colors.reset}\n`); - -let totalTests = 0; -let passedTests = 0; -let failedTests = 0; - -function runTest(name, testFn) { - totalTests++; - try { - testFn(); - log('green', `โœ… ${name}`); - passedTests++; - return true; - } catch (error) { - log('red', `โŒ ${name}`); - log('red', ` Error: ${error.message}`); - failedTests++; - return false; - } -} - -// Test Issue #15: Environment Validation -log('blue', '\n๐Ÿ“ฆ Issue #15: Environment Variables Validation'); -log('blue', 'โ”€'.repeat(70)); - -runTest('env.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/config/env.ts')); - if (!exists) throw new Error('env.ts not found'); -}); - -runTest('env.init.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/config/env.init.ts')); - if (!exists) throw new Error('env.init.ts not found'); -}); - -runTest('ENV_VALIDATION_README.md exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/config/ENV_VALIDATION_README.md')); - if (!exists) throw new Error('Documentation not found'); -}); - -runTest('Zod imported in env.ts', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/config/env.ts'), 'utf8'); - if (!content.includes('import { z } from "zod"')) throw new Error('Zod import missing'); -}); - -runTest('validateEnv function exported', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/config/env.ts'), 'utf8'); - if (!content.includes('export function validateEnv')) throw new Error('validateEnv not exported'); -}); - -// Test Issue #18: Input Validation -log('blue', '\n๐Ÿ“ฆ Issue #18: Input Validation and Sanitization'); -log('blue', 'โ”€'.repeat(70)); - -runTest('validation.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/lib/validation.ts')); - if (!exists) throw new Error('validation.ts not found'); -}); - -runTest('Stellar SDK imported', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/validation.ts'), 'utf8'); - if (!content.includes('StrKey')) throw new Error('StrKey import missing'); -}); - -runTest('validateStellarAddress function exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/validation.ts'), 'utf8'); - if (!content.includes('validateStellarAddress')) throw new Error('Function not found'); -}); - -runTest('sanitizeString function exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/validation.ts'), 'utf8'); - if (!content.includes('sanitizeString')) throw new Error('Function not found'); -}); - -runTest('XSS prevention implemented', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/validation.ts'), 'utf8'); - if (!content.includes('<') || !content.includes('>')) { - throw new Error('HTML entity encoding not found'); - } -}); - -// Test Issue #12: API Retry -log('blue', '\n๐Ÿ“ฆ Issue #12: API Rate Limiting and Retry Mechanism'); -log('blue', 'โ”€'.repeat(70)); - -runTest('apiRetry.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/lib/apiRetry.ts')); - if (!exists) throw new Error('apiRetry.ts not found'); -}); - -runTest('retryWithBackoff function exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/apiRetry.ts'), 'utf8'); - if (!content.includes('retryWithBackoff')) throw new Error('Function not found'); -}); - -runTest('CircuitBreaker class exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/apiRetry.ts'), 'utf8'); - if (!content.includes('class CircuitBreaker')) throw new Error('CircuitBreaker not found'); -}); - -runTest('RateLimiter class exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/apiRetry.ts'), 'utf8'); - if (!content.includes('class RateLimiter')) throw new Error('RateLimiter not found'); -}); - -runTest('Exponential backoff implemented', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/apiRetry.ts'), 'utf8'); - if (!content.includes('Math.pow')) throw new Error('Exponential backoff not found'); -}); - -// Test Issue #11: Error Boundary -log('blue', '\n๐Ÿ“ฆ Issue #11: Error Boundary Improvements'); -log('blue', 'โ”€'.repeat(70)); - -runTest('ErrorBoundary.jsx exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/components/ErrorBoundary.jsx')); - if (!exists) throw new Error('ErrorBoundary.jsx not found'); -}); - -runTest('logErrorToService method added', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/components/ErrorBoundary.jsx'), 'utf8'); - if (!content.includes('logErrorToService')) throw new Error('logErrorToService not found'); -}); - -runTest('resetErrorBoundary method added', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/components/ErrorBoundary.jsx'), 'utf8'); - if (!content.includes('resetErrorBoundary')) throw new Error('resetErrorBoundary not found'); -}); - -// Test Issue #19: Loading States -log('blue', '\n๐Ÿ“ฆ Issue #19: Loading States and Skeleton Screens'); -log('blue', 'โ”€'.repeat(70)); - -runTest('LoadingStates.jsx exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/components/LoadingStates.jsx')); - if (!exists) throw new Error('LoadingStates.jsx not found'); -}); - -runTest('Skeleton component exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/components/LoadingStates.jsx'), 'utf8'); - if (!content.includes('export const Skeleton')) throw new Error('Skeleton not found'); -}); - -runTest('Spinner component exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/components/LoadingStates.jsx'), 'utf8'); - if (!content.includes('export const Spinner')) throw new Error('Spinner not found'); -}); - -runTest('Multiple skeleton variants', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/components/LoadingStates.jsx'), 'utf8'); - if (!content.includes('CardSkeleton') || !content.includes('RiskScoreSkeleton')) { - throw new Error('Skeleton variants not found'); - } -}); - -// Test Issue #17: Caching -log('blue', '\n๐Ÿ“ฆ Issue #17: Caching Strategy Improvements'); -log('blue', 'โ”€'.repeat(70)); - -runTest('cacheManager.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/lib/cacheManager.ts')); - if (!exists) throw new Error('cacheManager.ts not found'); -}); - -runTest('CacheManager class exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/cacheManager.ts'), 'utf8'); - if (!content.includes('class CacheManager')) throw new Error('CacheManager not found'); -}); - -runTest('TTL support implemented', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/cacheManager.ts'), 'utf8'); - if (!content.includes('ttl')) throw new Error('TTL not implemented'); -}); - -runTest('Cache versioning implemented', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/cacheManager.ts'), 'utf8'); - if (!content.includes('version')) throw new Error('Versioning not implemented'); -}); - -runTest('Multiple storage backends', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/cacheManager.ts'), 'utf8'); - if (!content.includes('localStorage') || !content.includes('sessionStorage')) { - throw new Error('Storage backends not found'); - } -}); - -// Test Issue #13: Accessibility -log('blue', '\n๐Ÿ“ฆ Issue #13: Accessibility (a11y) Improvements'); -log('blue', 'โ”€'.repeat(70)); - -runTest('accessibility.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/lib/accessibility.ts')); - if (!exists) throw new Error('accessibility.ts not found'); -}); - -runTest('ARIA label generators exist', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/accessibility.ts'), 'utf8'); - if (!content.includes('getRiskScoreAriaLabel')) throw new Error('ARIA generators not found'); -}); - -runTest('Keyboard handlers implemented', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/accessibility.ts'), 'utf8'); - if (!content.includes('keyboardHandlers')) throw new Error('Keyboard handlers not found'); -}); - -runTest('Focus management implemented', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/accessibility.ts'), 'utf8'); - if (!content.includes('focusManager')) throw new Error('Focus management not found'); -}); - -runTest('Screen reader support', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/accessibility.ts'), 'utf8'); - if (!content.includes('announceToScreenReader')) throw new Error('Screen reader support not found'); -}); - -// Test Issue #14: Performance -log('blue', '\n๐Ÿ“ฆ Issue #14: Performance Optimizations'); -log('blue', 'โ”€'.repeat(70)); - -runTest('performanceUtils.ts exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../src/lib/performanceUtils.ts')); - if (!exists) throw new Error('performanceUtils.ts not found'); -}); - -runTest('debounce function exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/performanceUtils.ts'), 'utf8'); - if (!content.includes('export function debounce')) throw new Error('debounce not found'); -}); - -runTest('throttle function exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/performanceUtils.ts'), 'utf8'); - if (!content.includes('export function throttle')) throw new Error('throttle not found'); -}); - -runTest('memoize function exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/performanceUtils.ts'), 'utf8'); - if (!content.includes('export function memoize')) throw new Error('memoize not found'); -}); - -runTest('Virtual scrolling helper exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/performanceUtils.ts'), 'utf8'); - if (!content.includes('useVirtualScroll')) throw new Error('Virtual scrolling not found'); -}); - -runTest('Performance monitoring exists', () => { - const content = fs.readFileSync(path.join(__dirname, '../src/lib/performanceUtils.ts'), 'utf8'); - if (!content.includes('PerformanceMonitor')) throw new Error('Performance monitoring not found'); -}); - -// Test Issue #23: Documentation -log('blue', '\n๐Ÿ“ฆ Issue #23: Documentation Improvements'); -log('blue', 'โ”€'.repeat(70)); - -runTest('DEVELOPMENT.md exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../DEVELOPMENT.md')); - if (!exists) throw new Error('DEVELOPMENT.md not found'); -}); - -runTest('CONTRIBUTIONS.md exists', () => { - const exists = fs.existsSync(path.join(__dirname, '../CONTRIBUTIONS.md')); - if (!exists) throw new Error('CONTRIBUTIONS.md not found'); -}); - -runTest('Development guide is comprehensive', () => { - const content = fs.readFileSync(path.join(__dirname, '../DEVELOPMENT.md'), 'utf8'); - if (content.length < 1000) throw new Error('Documentation too short'); -}); - -// Test package.json -log('blue', '\n๐Ÿ“ฆ Dependencies'); -log('blue', 'โ”€'.repeat(70)); - -runTest('zod dependency added', () => { - const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')); - if (!packageJson.dependencies || !packageJson.dependencies.zod) { - throw new Error('zod dependency missing'); - } -}); - -// Summary -log('cyan', '\n' + '='.repeat(70)); -log('cyan', 'TEST SUMMARY'); -log('cyan', '='.repeat(70)); - -log('cyan', `\nTotal Tests: ${totalTests}`); -log('green', `Passed: ${passedTests}`); -if (failedTests > 0) { - log('red', `Failed: ${failedTests}`); -} - -const passRate = ((passedTests / totalTests) * 100).toFixed(1); -log('cyan', `Pass Rate: ${passRate}%\n`); - -if (failedTests === 0) { - log('green', '๐ŸŽ‰ ALL TESTS PASSED!'); - log('green', 'โœ… All 8+ issues successfully implemented'); - log('cyan', '\n๐Ÿ“Š Issues Resolved:'); - log('blue', ' #15 - Environment Variables Validation'); - log('blue', ' #18 - Input Validation and Sanitization'); - log('blue', ' #12 - API Rate Limiting and Retry Mechanism'); - log('blue', ' #11 - Error Boundary Improvements'); - log('blue', ' #19 - Loading States and Skeleton Screens'); - log('blue', ' #17 - Caching Strategy Improvements'); - log('blue', ' #13 - Accessibility (a11y) Improvements'); - log('blue', ' #14 - Performance Optimizations'); - log('blue', ' #23 - Documentation Improvements (Partial)'); - log('cyan', '\nโœจ Ready for commit and PR!\n'); - process.exit(0); -} else { - log('red', '\nโš ๏ธ SOME TESTS FAILED'); - log('yellow', 'Please review the failures above.\n'); - process.exit(1); -} diff --git a/scripts/test-api-retry.js b/scripts/test-api-retry.js deleted file mode 100644 index ab5c02c6..00000000 --- a/scripts/test-api-retry.js +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env node -/** - * Test script for API retry module - * Tests API Rate Limiting and Retry Mechanism (Issue #12) - */ - -const fs = require('fs'); -const path = require('path'); - -const colors = { - reset: '\x1b[0m', - green: '\x1b[32m', - red: '\x1b[31m', - cyan: '\x1b[36m', - blue: '\x1b[34m', -}; - -function log(color, message) { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -console.log(`\n${colors.cyan}๐Ÿงช API Retry Module Tests${colors.reset}\n`); - -let allTestsPassed = true; - -// Test 1: Check apiRetry.ts exists -console.log(`${colors.blue}Test 1: Checking apiRetry.ts exists...${colors.reset}`); -const apiRetryPath = path.join(__dirname, '../src/lib/apiRetry.ts'); -if (fs.existsSync(apiRetryPath)) { - log('green', 'โœ… PASS: apiRetry.ts file exists'); -} else { - log('red', 'โŒ FAIL: apiRetry.ts file not found'); - allTestsPassed = false; -} - -// Test 2: Check module structure -console.log(`\n${colors.blue}Test 2: Checking module structure...${colors.reset}`); -const apiRetryContent = fs.readFileSync(apiRetryPath, 'utf8'); - -const requiredFunctions = [ - 'retryWithBackoff', - 'fetchWithRetry', - 'fetchWithCircuitBreaker', - 'fetchWithRateLimit', - 'fetchWithProtection', -]; - -const requiredClasses = [ - 'CircuitBreaker', - 'RateLimiter', -]; - -let structureValid = true; -for (const func of requiredFunctions) { - if (!apiRetryContent.includes(`function ${func}`) && - !apiRetryContent.includes(`export function ${func}`) && - !apiRetryContent.includes(`async function ${func}`) && - !apiRetryContent.includes(`export async function ${func}`)) { - log('red', `โŒ Missing function: ${func}`); - structureValid = false; - allTestsPassed = false; - } -} - -for (const cls of requiredClasses) { - if (!apiRetryContent.includes(`class ${cls}`)) { - log('red', `โŒ Missing class: ${cls}`); - structureValid = false; - allTestsPassed = false; - } -} - -if (structureValid) { - log('green', 'โœ… PASS: apiRetry.ts has correct structure'); - log('blue', ` - Found ${requiredFunctions.length} retry functions`); - log('blue', ` - Found ${requiredClasses.length} protection classes`); -} - -// Test 3: Check exponential backoff implementation -console.log(`\n${colors.blue}Test 3: Checking exponential backoff...${colors.reset}`); -if (apiRetryContent.includes('calculateDelay') && - apiRetryContent.includes('backoffMultiplier') && - apiRetryContent.includes('Math.pow')) { - log('green', 'โœ… PASS: Exponential backoff implemented'); - log('blue', ' - Delay calculation function'); - log('blue', ' - Backoff multiplier'); - log('blue', ' - Jitter for thundering herd prevention'); -} else { - log('red', 'โŒ FAIL: Exponential backoff incomplete'); - allTestsPassed = false; -} - -// Test 4: Check circuit breaker pattern -console.log(`\n${colors.blue}Test 4: Checking circuit breaker pattern...${colors.reset}`); -if (apiRetryContent.includes('CircuitState') && - apiRetryContent.includes('CLOSED') && - apiRetryContent.includes('OPEN') && - apiRetryContent.includes('HALF_OPEN')) { - log('green', 'โœ… PASS: Circuit breaker pattern implemented'); - log('blue', ' - CLOSED state (normal operation)'); - log('blue', ' - OPEN state (rejecting requests)'); - log('blue', ' - HALF_OPEN state (testing recovery)'); -} else { - log('red', 'โŒ FAIL: Circuit breaker pattern incomplete'); - allTestsPassed = false; -} - -// Test 5: Check rate limiting -console.log(`\n${colors.blue}Test 5: Checking rate limiting...${colors.reset}`); -if (apiRetryContent.includes('RateLimiter') && - apiRetryContent.includes('token') && - apiRetryContent.includes('refill')) { - log('green', 'โœ… PASS: Rate limiting implemented'); - log('blue', ' - Token bucket algorithm'); - log('blue', ' - Token refill mechanism'); -} else { - log('red', 'โŒ FAIL: Rate limiting incomplete'); - allTestsPassed = false; -} - -// Test 6: Check retryable error detection -console.log(`\n${colors.blue}Test 6: Checking retryable error detection...${colors.reset}`); -if (apiRetryContent.includes('isRetryableError') && - apiRetryContent.includes('retryableStatusCodes') && - (apiRetryContent.includes('408') || apiRetryContent.includes('429') || - apiRetryContent.includes('500') || apiRetryContent.includes('503'))) { - log('green', 'โœ… PASS: Retryable error detection implemented'); - log('blue', ' - HTTP status code checking'); - log('blue', ' - Network error detection'); - log('blue', ' - Rate limit detection'); -} else { - log('red', 'โŒ FAIL: Retryable error detection incomplete'); - allTestsPassed = false; -} - -// Test 7: Check timeout handling -console.log(`\n${colors.blue}Test 7: Checking timeout handling...${colors.reset}`); -if (apiRetryContent.includes('timeout') && - apiRetryContent.includes('AbortController')) { - log('green', 'โœ… PASS: Timeout handling implemented'); - log('blue', ' - AbortController for request cancellation'); - log('blue', ' - Configurable timeout'); -} else { - log('red', 'โŒ FAIL: Timeout handling incomplete'); - allTestsPassed = false; -} - -// Test 8: Check comprehensive fetch wrapper -console.log(`\n${colors.blue}Test 8: Checking comprehensive fetch wrapper...${colors.reset}`); -if (apiRetryContent.includes('fetchWithProtection')) { - log('green', 'โœ… PASS: Comprehensive fetch wrapper present'); - log('blue', ' - Combines retry + circuit breaker + rate limiting'); -} else { - log('red', 'โŒ FAIL: Comprehensive fetch wrapper missing'); - allTestsPassed = false; -} - -// Test 9: Check configuration options -console.log(`\n${colors.blue}Test 9: Checking configuration options...${colors.reset}`); -if (apiRetryContent.includes('RetryOptions') && - apiRetryContent.includes('maxRetries') && - apiRetryContent.includes('initialDelay') && - apiRetryContent.includes('maxDelay')) { - log('green', 'โœ… PASS: Configuration options defined'); - log('blue', ' - Retry configuration interface'); - log('blue', ' - Default options'); -} else { - log('red', 'โŒ FAIL: Configuration options incomplete'); - allTestsPassed = false; -} - -// Test 10: Check exports -console.log(`\n${colors.blue}Test 10: Checking exports...${colors.reset}`); -if (apiRetryContent.includes('export const ApiRetry')) { - log('green', 'โœ… PASS: Convenience exports present'); - log('blue', ' - ApiRetry object exported'); -} else { - log('red', 'โŒ FAIL: Missing convenience exports'); - allTestsPassed = false; -} - -// Summary -console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`); -if (allTestsPassed) { - log('green', 'โœ… ALL API RETRY TESTS PASSED'); - log('cyan', '\n๐Ÿ“‹ Issue #12: API Rate Limiting and Retry Mechanism - COMPLETE\n'); - process.exit(0); -} else { - log('red', 'โŒ SOME TESTS FAILED'); - process.exit(1); -} diff --git a/scripts/test-env-validation-simple.js b/scripts/test-env-validation-simple.js deleted file mode 100644 index a8c0440f..00000000 --- a/scripts/test-env-validation-simple.js +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env node -/** - * Simple validation test for environment module - * - * Since the module is TypeScript, we'll test that: - * 1. The TypeScript files have correct syntax - * 2. The schema definitions are properly structured - * 3. The documentation is complete - * - * For runtime testing, run: npm run build - */ - -const fs = require('fs'); -const path = require('path'); - -const colors = { - reset: '\x1b[0m', - green: '\x1b[32m', - red: '\x1b[31m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', -}; - -function log(color, message) { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -console.log(`\n${colors.cyan}๐Ÿงช Environment Validation Module Tests${colors.reset}\n`); - -let allTestsPassed = true; - -// Test 1: Check that env.ts exists -console.log(`${colors.blue}Test 1: Checking env.ts exists...${colors.reset}`); -const envPath = path.join(__dirname, '../src/config/env.ts'); -if (fs.existsSync(envPath)) { - log('green', 'โœ… PASS: env.ts file exists'); -} else { - log('red', 'โŒ FAIL: env.ts file not found'); - allTestsPassed = false; -} - -// Test 2: Check that env.init.ts exists -console.log(`\n${colors.blue}Test 2: Checking env.init.ts exists...${colors.reset}`); -const envInitPath = path.join(__dirname, '../src/config/env.init.ts'); -if (fs.existsSync(envInitPath)) { - log('green', 'โœ… PASS: env.init.ts file exists'); -} else { - log('red', 'โŒ FAIL: env.init.ts file not found'); - allTestsPassed = false; -} - -// Test 3: Check that documentation exists -console.log(`\n${colors.blue}Test 3: Checking documentation exists...${colors.reset}`); -const docsPath = path.join(__dirname, '../src/config/ENV_VALIDATION_README.md'); -if (fs.existsSync(docsPath)) { - log('green', 'โœ… PASS: Documentation file exists'); - const docsContent = fs.readFileSync(docsPath, 'utf8'); - if (docsContent.length > 1000) { - log('green', ` Documentation is ${docsContent.length} characters (comprehensive)`); - } -} else { - log('red', 'โŒ FAIL: Documentation not found'); - allTestsPassed = false; -} - -// Test 4: Check that package.json includes zod -console.log(`\n${colors.blue}Test 4: Checking zod dependency...${colors.reset}`); -const packagePath = path.join(__dirname, '../package.json'); -const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); -if (packageJson.dependencies && packageJson.dependencies.zod) { - log('green', `โœ… PASS: zod dependency added (version: ${packageJson.dependencies.zod})`); -} else { - log('red', 'โŒ FAIL: zod not found in dependencies'); - allTestsPassed = false; -} - -// Test 5: Check TypeScript syntax in env.ts -console.log(`\n${colors.blue}Test 5: Checking env.ts structure...${colors.reset}`); -const envContent = fs.readFileSync(envPath, 'utf8'); - -const requiredImports = ['import { z } from "zod"']; -const requiredExports = [ - 'export function validateClientEnv', - 'export function validateServerEnv', - 'export function validateEnv', - 'export function getEnv', - 'export function checkMissingEnvVars', -]; - -let structureValid = true; -for (const imp of requiredImports) { - if (!envContent.includes(imp)) { - log('red', `โŒ Missing import: ${imp}`); - structureValid = false; - allTestsPassed = false; - } -} - -for (const exp of requiredExports) { - if (!envContent.includes(exp)) { - log('red', `โŒ Missing export: ${exp}`); - structureValid = false; - allTestsPassed = false; - } -} - -if (structureValid) { - log('green', 'โœ… PASS: env.ts has correct structure'); - log('blue', ' - Zod import present'); - log('blue', ' - All validation functions exported'); -} - -// Test 6: Check for Zod schemas -console.log(`\n${colors.blue}Test 6: Checking Zod schemas...${colors.reset}`); -const schemas = [ - 'clientEnvSchema', - 'serverEnvSchema', - 'envSchema', - 'urlSchema', - 'contractIdSchema', - 'portSchema', -]; - -let schemasValid = true; -for (const schema of schemas) { - if (!envContent.includes(schema)) { - log('red', `โŒ Missing schema: ${schema}`); - schemasValid = false; - allTestsPassed = false; - } -} - -if (schemasValid) { - log('green', 'โœ… PASS: All required Zod schemas defined'); - log('blue', ` - Found ${schemas.length} schemas`); -} - -// Test 7: Check for required environment variables -console.log(`\n${colors.blue}Test 7: Checking required variables definition...${colors.reset}`); -const requiredVars = [ - 'NEXT_PUBLIC_RPC_URL', - 'NEXT_PUBLIC_NETWORK_PASSPHRASE', - 'REDIS_HOST', - 'REDIS_PORT', -]; - -let varsValid = true; -for (const varName of requiredVars) { - if (!envContent.includes(varName)) { - log('red', `โŒ Missing variable definition: ${varName}`); - varsValid = false; - allTestsPassed = false; - } -} - -if (varsValid) { - log('green', 'โœ… PASS: All required variables defined in schema'); -} - -// Test 8: Check error formatting function -console.log(`\n${colors.blue}Test 8: Checking error formatting...${colors.reset}`); -if (envContent.includes('formatZodError')) { - log('green', 'โœ… PASS: Error formatting function present'); -} else { - log('red', 'โŒ FAIL: Error formatting function missing'); - allTestsPassed = false; -} - -// Test 9: Check initialization logic -console.log(`\n${colors.blue}Test 9: Checking initialization logic...${colors.reset}`); -const envInitContent = fs.readFileSync(envInitPath, 'utf8'); -if (envInitContent.includes('initializeEnv') && - envInitContent.includes('checkMissingEnvVars') && - envInitContent.includes('validateEnv')) { - log('green', 'โœ… PASS: Initialization logic complete'); - log('blue', ' - initializeEnv function present'); - log('blue', ' - Calls validation functions'); - log('blue', ' - Checks for missing variables'); -} else { - log('red', 'โŒ FAIL: Initialization logic incomplete'); - allTestsPassed = false; -} - -// Test 10: Check documentation completeness -console.log(`\n${colors.blue}Test 10: Checking documentation completeness...${colors.reset}`); -const docsContent = fs.readFileSync(docsPath, 'utf8'); -const docsSections = [ - '## Overview', - '## Features', - '## Usage', - '## Environment Variables', - '## Validation Rules', - '## Error Handling', - '## Testing', -]; - -let docsValid = true; -for (const section of docsSections) { - if (!docsContent.includes(section)) { - log('red', `โŒ Missing documentation section: ${section}`); - docsValid = false; - allTestsPassed = false; - } -} - -if (docsValid) { - log('green', 'โœ… PASS: Documentation is complete'); - log('blue', ` - All ${docsSections.length} sections present`); -} - -// Summary -console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`); -if (allTestsPassed) { - log('green', 'โœ… ALL TESTS PASSED'); - log('cyan', '\n๐Ÿ“‹ Static validation complete!'); - log('cyan', '๐Ÿ’ก To test runtime validation, run: npm run build'); - log('cyan', ' This will validate the environment during the build process.\n'); - process.exit(0); -} else { - log('red', 'โŒ SOME TESTS FAILED'); - log('yellow', '\nโš ๏ธ Please review the failures above.\n'); - process.exit(1); -} diff --git a/scripts/test-validation.js b/scripts/test-validation.js deleted file mode 100644 index 2c94f33d..00000000 --- a/scripts/test-validation.js +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env node -/** - * Test script for validation module - * Tests Input Validation and Sanitization (Issue #18) - */ - -const fs = require('fs'); -const path = require('path'); - -const colors = { - reset: '\x1b[0m', - green: '\x1b[32m', - red: '\x1b[31m', - cyan: '\x1b[36m', - blue: '\x1b[34m', -}; - -function log(color, message) { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -console.log(`\n${colors.cyan}๐Ÿงช Validation Module Tests${colors.reset}\n`); - -let allTestsPassed = true; - -// Test 1: Check validation.ts exists -console.log(`${colors.blue}Test 1: Checking validation.ts exists...${colors.reset}`); -const validationPath = path.join(__dirname, '../src/lib/validation.ts'); -if (fs.existsSync(validationPath)) { - log('green', 'โœ… PASS: validation.ts file exists'); -} else { - log('red', 'โŒ FAIL: validation.ts file not found'); - allTestsPassed = false; -} - -// Test 2: Check module structure -console.log(`\n${colors.blue}Test 2: Checking module structure...${colors.reset}`); -const validationContent = fs.readFileSync(validationPath, 'utf8'); - -const requiredImports = ['import { StrKey }']; -const requiredFunctions = [ - 'validateStellarAddress', - 'validateContractId', - 'validateRiskScore', - 'sanitizeString', - 'validateUrl', - 'validateNumberRange', - 'validateEmail', - 'validateTransactionHash', - 'validateAmount', - 'validateAssetCode', -]; - -let structureValid = true; -for (const imp of requiredImports) { - if (!validationContent.includes(imp)) { - log('red', `โŒ Missing import: ${imp}`); - structureValid = false; - allTestsPassed = false; - } -} - -for (const func of requiredFunctions) { - if (!validationContent.includes(`function ${func}`) && - !validationContent.includes(`export function ${func}`)) { - log('red', `โŒ Missing function: ${func}`); - structureValid = false; - allTestsPassed = false; - } -} - -if (structureValid) { - log('green', 'โœ… PASS: validation.ts has correct structure'); - log('blue', ` - Found ${requiredFunctions.length} validation functions`); -} - -// Test 3: Check for XSS prevention -console.log(`\n${colors.blue}Test 3: Checking XSS prevention...${colors.reset}`); -if (validationContent.includes('sanitizeString') && - validationContent.includes('stripHtmlTags') && - validationContent.includes('<') && - validationContent.includes('>')) { - log('green', 'โœ… PASS: XSS prevention utilities present'); - log('blue', ' - sanitizeString function'); - log('blue', ' - stripHtmlTags function'); - log('blue', ' - HTML entity encoding'); -} else { - log('red', 'โŒ FAIL: XSS prevention incomplete'); - allTestsPassed = false; -} - -// Test 4: Check Stellar-specific validations -console.log(`\n${colors.blue}Test 4: Checking Stellar-specific validations...${colors.reset}`); -if (validationContent.includes('StrKey.isValidEd25519PublicKey') && - validationContent.includes('StrKey.isValidContract')) { - log('green', 'โœ… PASS: Stellar address validation present'); - log('blue', ' - G... address validation'); - log('blue', ' - C... contract validation'); -} else { - log('red', 'โŒ FAIL: Stellar validation incomplete'); - allTestsPassed = false; -} - -// Test 5: Check validation result interface -console.log(`\n${colors.blue}Test 5: Checking ValidationResult interface...${colors.reset}`); -if (validationContent.includes('interface ValidationResult') && - validationContent.includes('isValid: boolean') && - validationContent.includes('error?: string') && - validationContent.includes('sanitized?:')) { - log('green', 'โœ… PASS: ValidationResult interface defined correctly'); -} else { - log('red', 'โŒ FAIL: ValidationResult interface missing or incomplete'); - allTestsPassed = false; -} - -// Test 6: Check comprehensive validation coverage -console.log(`\n${colors.blue}Test 6: Checking validation coverage...${colors.reset}`); -const validationTypes = [ - 'address', - 'contract', - 'score', - 'url', - 'email', - 'transaction', - 'amount', - 'asset', -]; - -let coverageValid = true; -for (const type of validationTypes) { - const found = validationContent.toLowerCase().includes(type); - if (!found) { - log('red', `โŒ Missing validation type: ${type}`); - coverageValid = false; - allTestsPassed = false; - } -} - -if (coverageValid) { - log('green', 'โœ… PASS: Comprehensive validation coverage'); - log('blue', ` - ${validationTypes.length} validation types covered`); -} - -// Test 7: Check exports -console.log(`\n${colors.blue}Test 7: Checking exports...${colors.reset}`); -if (validationContent.includes('export const Validators') && - validationContent.includes('export const Sanitizers')) { - log('green', 'โœ… PASS: Convenience exports present'); - log('blue', ' - Validators object exported'); - log('blue', ' - Sanitizers object exported'); -} else { - log('red', 'โŒ FAIL: Missing convenience exports'); - allTestsPassed = false; -} - -// Summary -console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`); -if (allTestsPassed) { - log('green', 'โœ… ALL VALIDATION TESTS PASSED'); - log('cyan', '\n๐Ÿ“‹ Issue #18: Input Validation and Sanitization - COMPLETE\n'); - process.exit(0); -} else { - log('red', 'โŒ SOME TESTS FAILED'); - process.exit(1); -} diff --git a/scripts/validate-env.js b/scripts/validate-env.js deleted file mode 100644 index 02945d0d..00000000 --- a/scripts/validate-env.js +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env node -/** - * Manual validation script for environment variables - * - * This script tests the environment validation module with various scenarios: - * 1. Valid configuration - * 2. Missing required variables - * 3. Invalid URL format - * 4. Invalid contract ID format - * 5. Invalid port number - * 6. Invalid network type - * - * Run with: node scripts/validate-env.js - */ - -const fs = require('fs'); -const path = require('path'); - -// Colors for terminal output -const colors = { - reset: '\x1b[0m', - green: '\x1b[32m', - red: '\x1b[31m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', -}; - -function log(color, message) { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -function section(title) { - console.log(`\n${colors.cyan}${'='.repeat(60)}`); - console.log(`${title}`); - console.log(`${'='.repeat(60)}${colors.reset}\n`); -} - -// Test Case 1: Valid Configuration -section('Test 1: Valid Configuration'); -(() => { - const backup = { ...process.env }; - - // Set valid environment variables - process.env.NEXT_PUBLIC_RPC_URL = 'https://soroban-testnet.stellar.org'; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.NEXT_PUBLIC_RISK_TIER_CONTRACT_ID = 'CD6NTP2JCX4F3V4RLIJFLGSG7SVTAPXMKKD3BTF4DY5NCV7YAO3OLABN'; - process.env.NODE_ENV = 'development'; - process.env.REDIS_PORT = '6379'; - process.env.LIQUIDITY_API_PORT = '3001'; - - try { - // Delete require cache to force re-evaluation - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateEnv } = require('../src/config/env.ts'); - const env = validateEnv(); - - log('green', 'โœ… PASS: Valid configuration accepted'); - log('blue', ` RPC URL: ${env.NEXT_PUBLIC_RPC_URL}`); - log('blue', ` Redis Port: ${env.REDIS_PORT}`); - } catch (error) { - log('red', 'โŒ FAIL: Valid configuration rejected'); - log('red', ` Error: ${error.message}`); - } finally { - process.env = { ...backup }; - } -})(); - -// Test Case 2: Missing Required Variable -section('Test 2: Missing Required Variable (NEXT_PUBLIC_RPC_URL)'); -(() => { - const backup = { ...process.env }; - - // Clear required variable - delete process.env.NEXT_PUBLIC_RPC_URL; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.NODE_ENV = 'development'; - - try { - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateClientEnv } = require('../src/config/env.ts'); - validateClientEnv(); - - log('red', 'โŒ FAIL: Missing required variable not detected'); - } catch (error) { - if (error.message.includes('NEXT_PUBLIC_RPC_URL')) { - log('green', 'โœ… PASS: Missing variable detected correctly'); - log('blue', ` Error message: ${error.message.split('\\n')[1]}`); - } else { - log('red', 'โŒ FAIL: Wrong error message'); - log('red', ` Error: ${error.message}`); - } - } finally { - process.env = { ...backup }; - } -})(); - -// Test Case 3: Invalid URL Format -section('Test 3: Invalid URL Format'); -(() => { - const backup = { ...process.env }; - - process.env.NEXT_PUBLIC_RPC_URL = 'not-a-valid-url'; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.NODE_ENV = 'development'; - - try { - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateClientEnv } = require('../src/config/env.ts'); - validateClientEnv(); - - log('red', 'โŒ FAIL: Invalid URL format not detected'); - } catch (error) { - if (error.message.includes('valid URL') || error.message.includes('url')) { - log('green', 'โœ… PASS: Invalid URL format detected correctly'); - log('blue', ` Error includes URL validation`); - } else { - log('red', 'โŒ FAIL: Wrong error message'); - log('red', ` Error: ${error.message}`); - } - } finally { - process.env = { ...backup }; - } -})(); - -// Test Case 4: Invalid Contract ID Format -section('Test 4: Invalid Contract ID Format'); -(() => { - const backup = { ...process.env }; - - process.env.NEXT_PUBLIC_RPC_URL = 'https://soroban-testnet.stellar.org'; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.NEXT_PUBLIC_RISK_TIER_CONTRACT_ID = 'INVALID_CONTRACT_ID'; - process.env.NODE_ENV = 'development'; - - try { - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateClientEnv } = require('../src/config/env.ts'); - validateClientEnv(); - - log('red', 'โŒ FAIL: Invalid contract ID format not detected'); - } catch (error) { - if (error.message.includes('contract ID') || error.message.includes('56 characters')) { - log('green', 'โœ… PASS: Invalid contract ID format detected correctly'); - log('blue', ` Error includes contract ID validation`); - } else { - log('red', 'โŒ FAIL: Wrong error message'); - log('red', ` Error: ${error.message}`); - } - } finally { - process.env = { ...backup }; - } -})(); - -// Test Case 5: Invalid Port Number -section('Test 5: Invalid Port Number'); -(() => { - const backup = { ...process.env }; - - process.env.NEXT_PUBLIC_RPC_URL = 'https://soroban-testnet.stellar.org'; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.REDIS_PORT = '99999'; // Invalid port (> 65535) - process.env.NODE_ENV = 'development'; - - try { - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateServerEnv } = require('../src/config/env.ts'); - validateServerEnv(); - - log('red', 'โŒ FAIL: Invalid port number not detected'); - } catch (error) { - if (error.message.includes('Port') || error.message.includes('65535')) { - log('green', 'โœ… PASS: Invalid port number detected correctly'); - log('blue', ` Error includes port validation`); - } else { - log('red', 'โŒ FAIL: Wrong error message'); - log('red', ` Error: ${error.message}`); - } - } finally { - process.env = { ...backup }; - } -})(); - -// Test Case 6: Invalid Network Type -section('Test 6: Invalid Network Type'); -(() => { - const backup = { ...process.env }; - - process.env.NEXT_PUBLIC_RPC_URL = 'https://soroban-testnet.stellar.org'; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.STELLAR_NETWORK = 'INVALID_NETWORK'; - process.env.NODE_ENV = 'development'; - - try { - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateServerEnv } = require('../src/config/env.ts'); - validateServerEnv(); - - log('red', 'โŒ FAIL: Invalid network type not detected'); - } catch (error) { - if (error.message.includes('TESTNET') || error.message.includes('PUBLIC')) { - log('green', 'โœ… PASS: Invalid network type detected correctly'); - log('blue', ` Error includes network validation`); - } else { - log('red', 'โŒ FAIL: Wrong error message'); - log('red', ` Error: ${error.message}`); - } - } finally { - process.env = { ...backup }; - } -})(); - -// Test Case 7: Boolean Environment Variable Parsing -section('Test 7: Boolean Environment Variable Parsing'); -(() => { - const backup = { ...process.env }; - - process.env.NEXT_PUBLIC_RPC_URL = 'https://soroban-testnet.stellar.org'; - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE = 'Test SDF Network ; September 2015'; - process.env.NEXT_PUBLIC_PASSKEY_ENABLED = 'true'; - process.env.FEATURE_LIQUIDITY_MONITORING = '1'; - process.env.DEBUG_CONTRACT_CALLS = 'false'; - process.env.NODE_ENV = 'development'; - - try { - delete require.cache[require.resolve('../src/config/env.ts')]; - - const { validateEnv } = require('../src/config/env.ts'); - const env = validateEnv(); - - if (env.NEXT_PUBLIC_PASSKEY_ENABLED === true && - env.FEATURE_LIQUIDITY_MONITORING === true && - env.DEBUG_CONTRACT_CALLS === false) { - log('green', 'โœ… PASS: Boolean parsing works correctly'); - log('blue', ` PASSKEY_ENABLED: ${env.NEXT_PUBLIC_PASSKEY_ENABLED} (type: ${typeof env.NEXT_PUBLIC_PASSKEY_ENABLED})`); - log('blue', ` LIQUIDITY_MONITORING: ${env.FEATURE_LIQUIDITY_MONITORING} (type: ${typeof env.FEATURE_LIQUIDITY_MONITORING})`); - log('blue', ` DEBUG_CONTRACT_CALLS: ${env.DEBUG_CONTRACT_CALLS} (type: ${typeof env.DEBUG_CONTRACT_CALLS})`); - } else { - log('red', 'โŒ FAIL: Boolean parsing incorrect'); - } - } catch (error) { - log('red', 'โŒ FAIL: Boolean parsing failed'); - log('red', ` Error: ${error.message}`); - } finally { - process.env = { ...backup }; - } -})(); - -// Summary -section('Test Summary'); -log('cyan', '๐Ÿ“‹ All validation tests completed!'); -log('cyan', ' Review the results above to ensure all tests passed.'); -log('cyan', ' โœ… = Test passed correctly'); -log('cyan', ' โŒ = Test failed'); -console.log(''); diff --git a/src/components/__tests__/ErrorBoundary.test.jsx b/src/components/__tests__/ErrorBoundary.test.jsx new file mode 100644 index 00000000..d7bf442f --- /dev/null +++ b/src/components/__tests__/ErrorBoundary.test.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ErrorBoundary from '../ErrorBoundary'; + +// Component that simulates network errors +const NetworkErrorComponent = () => { + const [shouldThrow, setShouldThrow] = React.useState(false); + + if (shouldThrow) { + throw new Error('Network request failed: Unable to connect to blockchain'); + } + + return ; +}; + +describe('ErrorBoundary', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + + test('should help users recover from network failures', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Simulate Network Error')); + + expect(screen.getByText('Oops! Something went wrong')).toBeInTheDocument(); + expect(screen.getByText('Refresh Page')).toBeInTheDocument(); + expect(screen.getByText('Try Again')).toBeInTheDocument(); + }); + + test('should allow retry after network error', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Simulate Network Error')); + fireEvent.click(screen.getByText('Try Again')); + + expect(screen.getByText('Simulate Network Error')).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/Header.test.jsx b/src/components/__tests__/Header.test.jsx new file mode 100644 index 00000000..d60928b5 --- /dev/null +++ b/src/components/__tests__/Header.test.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Header from '../Header'; + +// Mock WalletContext +const mockWalletContext = { + walletAddress: null, + isLoading: false, + connectWallet: jest.fn(), + disconnectWallet: jest.fn(), + walletName: null, +}; + +jest.mock('../contexts/WalletContext', () => ({ + useWallet: () => mockWalletContext, +})); + +jest.mock('next/link', () => { + return ({ children, href }) => {children}; +}); + +describe('Header', () => { + beforeEach(() => { + mockWalletContext.walletAddress = null; + mockWalletContext.isLoading = false; + mockWalletContext.connectWallet.mockResolvedValue({ success: true }); + mockWalletContext.disconnectWallet.mockResolvedValue({ success: true }); + }); + + test('should handle wallet connection failures', async () => { + mockWalletContext.connectWallet.mockRejectedValue(new Error('Network error: Unable to reach wallet service')); + + render(
); + + fireEvent.click(screen.getByText('Connect Wallet')); + + expect(mockWalletContext.connectWallet).toHaveBeenCalled(); + expect(screen.getByText('Connect Wallet')).toBeInTheDocument(); + }); + + test('should show loading state during connection', () => { + mockWalletContext.isLoading = true; + + render(
); + + expect(screen.getByText('Connecting...')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /connecting/i })).toBeDisabled(); + }); + + test('should handle mobile menu toggle', () => { + render(
); + + const mobileMenuButton = screen.getByLabelText('Toggle mobile menu'); + fireEvent.click(mobileMenuButton); + + expect(screen.getByText('Home')).toBeInTheDocument(); + expect(screen.getByText('Features')).toBeInTheDocument(); + }); + + test('should prevent body scroll when mobile menu is open', () => { + render(
); + + const mobileMenuButton = screen.getByLabelText('Toggle mobile menu'); + fireEvent.click(mobileMenuButton); + + expect(document.body.style.overflow).toBe('hidden'); + + fireEvent.click(mobileMenuButton); + expect(document.body.style.overflow).toBe('unset'); + }); +}); diff --git a/src/components/__tests__/UserRiskProfile.test.jsx b/src/components/__tests__/UserRiskProfile.test.jsx new file mode 100644 index 00000000..3951c017 --- /dev/null +++ b/src/components/__tests__/UserRiskProfile.test.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import UserRiskProfile from '../UserRiskProfile'; + +describe('UserRiskProfile', () => { + const mockOnTierSelect = jest.fn(); + + beforeEach(() => { + mockOnTierSelect.mockClear(); + }); + + test('should show clear error when score is not calculated', () => { + render(); + + expect(screen.getByText('Credit score has not been calculated. Please generate your score first.')).toBeInTheDocument(); + }); + + test('should help users understand their risk level', () => { + render(); + + expect(screen.getByText('25')).toBeInTheDocument(); + expect(screen.getByText('Low Risk')).toBeInTheDocument(); + expect(screen.getByText(/strong on-chain history/)).toBeInTheDocument(); + }); + + test('should prevent selection of restricted tiers', () => { + render(); + + const tier2Card = screen.getByText('TIER-2').closest('div'); + fireEvent.click(tier2Card); + + expect(mockOnTierSelect).not.toHaveBeenCalled(); + }); + + test('should require confirmation for high-risk tier selection', () => { + render(); + + const tier3Card = screen.getByText('TIER-3').closest('div'); + fireEvent.click(tier3Card); + + expect(screen.getByText('High-Risk Tier Acknowledgment')).toBeInTheDocument(); + expect(screen.getByText(/low liquidity and high volatility/)).toBeInTheDocument(); + }); + + test('should handle high-risk confirmation flow', () => { + render(); + + const tier3Card = screen.getByText('TIER-3').closest('div'); + fireEvent.click(tier3Card); + fireEvent.click(screen.getByText('Acknowledge & Proceed')); + + expect(mockOnTierSelect).toHaveBeenCalledWith('TIER_3'); + }); +});