diff --git a/PUSH_STATUS.md b/PUSH_STATUS.md deleted file mode 100644 index 1b6b663..0000000 --- a/PUSH_STATUS.md +++ /dev/null @@ -1,85 +0,0 @@ -# GitHub Repository Push Status ✅ - -## Successfully Pushed All Completed Branches - -### ✅ **Branches Pushed to Remote Repository** - -#### 1. `issue/1-fix-piggybank-parameter-swap` -**Status**: ✅ Successfully pushed -**PR URL**: https://github.com/Oluwatomilola/ajo/pull/new/issue/1-fix-piggybank-parameter-swap -**Commits**: 1 -**Changes**: Fixed critical parameter swap bug in usePiggyBank hook -**Files Modified**: -- `frontend/src/hooks/usePiggyBank.ts` - -#### 2. `issue/2-fix-abi-mismatch` -**Status**: ✅ Successfully pushed -**PR URL**: https://github.com/Oluwatomilola/ajo/pull/new/issue/2-fix-abi-mismatch -**Commits**: 1 -**Changes**: Updated ABI to include all contract functions -**Files Modified**: -- `frontend/src/config/contracts.ts` - -#### 3. `issue/4-fix-input-validation-vulnerability` -**Status**: ✅ Successfully pushed -**PR URL**: https://github.com/Oluwatomilola/ajo/pull/new/issue/4-fix-input-validation-vulnerability -**Commits**: 1 -**Changes**: Fixed DOM manipulation security vulnerability -**Files Modified**: -- `frontend/src/components/PiggyBankDashboard.tsx` -- `frontend/src/components/DepositForm.tsx` - -#### 4. `issue/6-add-input-sanitization` -**Status**: ✅ Successfully pushed -**PR URL**: https://github.com/Oluwatomilola/ajo/pull/new/issue/6-add-input-sanitization -**Commits**: 2 (including documentation commit) -**Changes**: Added comprehensive input validation and sanitization -**Files Created/Modified**: -- `frontend/src/utils/validation.ts` (NEW) -- `frontend/src/components/PiggyBankDashboard.tsx` -- `frontend/src/components/DepositForm.tsx` -- `IMPLEMENTATION_SUMMARY.md` - -### 📊 **Push Statistics** -- **Branches Created**: 4 -- **Branches Pushed**: 4 (100%) -- **Total Commits**: 5 -- **Files Created**: 2 -- **Files Modified**: 5 -- **Pull Requests Available**: 4 (auto-generated URLs) - -### 🔗 **Ready for Review** -All branches are now available on GitHub with ready-to-use pull request URLs. Each branch contains: -- ✅ Complete, tested fixes -- ✅ Meaningful commit messages -- ✅ No breaking changes -- ✅ Backward compatibility maintained -- ✅ TypeScript validation passed - -### 📋 **Repository Structure on GitHub** -``` -origin/ -├── main (current state) -├── issue/1-fix-piggybank-parameter-swap ✅ -├── issue/2-fix-abi-mismatch ✅ -├── issue/4-fix-input-validation-vulnerability ✅ -├── issue/6-add-input-sanitization ✅ -└── [other existing branches] -``` - -### 🎯 **Next Steps for Review** -1. **Visit each PR URL** to create pull requests on GitHub -2. **Review changes** using GitHub's diff viewer -3. **Run tests** to validate fixes work correctly -4. **Merge** approved PRs to main branch -5. **Continue with remaining issues** if desired - -### ✅ **Quality Assurance Complete** -- All branches compile without errors -- TypeScript type checking passes -- No linting issues introduced -- Hot module replacement works -- Core functionality preserved -- Security vulnerabilities addressed - -**Status**: Ready for team review and merge process. All critical and high priority fixes have been successfully implemented and pushed to the remote repository. \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 5e6b472..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 072a57e..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - frontend - - -
- - - diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index db17837..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview", - "type-check": "tsc -b --noEmit", - "clean": "rm -rf dist node_modules/.vite", - "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", - "check-env": "node -e \"console.log('✅ Environment check passed')\" || echo '⚠️ Set up your .env file'", - "test": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest --coverage" - }, - "dependencies": { - "@heroicons/react": "^2.2.0", - "@reown/appkit": "^1.8.14", - "@reown/appkit-adapter-wagmi": "^1.8.14", - "@tanstack/react-query": "^5.90.8", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "viem": "^2.39.0", - "wagmi": "^2.19.3" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@playwright/test": "^1.57.0", - "@tailwindcss/postcss": "^4.1.17", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.10.0", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^5.1.0", - "@vitest/ui": "^4.0.10", - "buffer": "^6.0.3", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "jsdom": "^27.2.0", - "process": "^0.11.10", - "typescript": "~5.9.3", - "typescript-eslint": "^8.46.3", - "util": "^0.12.5", - "vite": "^7.2.2", - "vitest": "^4.0.10" - } -} diff --git a/frontend/src/hooks/usePiggyBank.ts b/frontend/src/hooks/usePiggyBank.ts index 9ae5806..e69de29 100644 --- a/frontend/src/hooks/usePiggyBank.ts +++ b/frontend/src/hooks/usePiggyBank.ts @@ -1,195 +0,0 @@ -import { useState } from 'react' -import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useWatchContractEvent } from 'wagmi' -import { parseEther } from 'viem' -import { useMemo, useCallback, useRef, useEffect } from 'react' -import { PIGGYBANK_ABI, PIGGYBANK_ADDRESS } from '../config/contracts' -import { TIME } from '../constants/appConstants' - -interface Transaction { - id: string; - amount: number; - timestamp: number; - type: 'deposit' | 'withdrawal'; - user: string; -} - -export function usePiggyBank() { - const { address } = useAccount() - const { writeContract, data: hash, isPending } = useWriteContract() - const [transactions, setTransactions] = useState([]) - const refetchTimeoutRef = useRef(null) - - // Memoize balance to prevent unnecessary re-renders - const { data: balance, refetch: refetchBalance } = useReadContract({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - functionName: 'getBalance', - }) - - // Memoize unlock time - const { data: unlockTime, refetch: refetchUnlockTime } = useReadContract({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - functionName: 'unlockTime', - }) - - // Memoize owner to prevent unnecessary re-renders - const { data: owner } = useReadContract({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - functionName: 'owner', - }) - - // Debounced refetch to prevent excessive network calls - const debouncedRefetch = useCallback(() => { - // Clear existing timeout - if (refetchTimeoutRef.current) { - clearTimeout(refetchTimeoutRef.current) - } - - // Set new timeout - refetchTimeoutRef.current = setTimeout(() => { - refetchBalance() - refetchTimeoutRef.current = null - }, TIME.DEBOUNCE_DELAY) // 1 second debounce - }, [refetchBalance]) - - // Cleanup timeout on unmount - useEffect(() => { - return () => { - if (refetchTimeoutRef.current) { - clearTimeout(refetchTimeoutRef.current) - refetchTimeoutRef.current = null - } - } - }, []) - - // Watch for Deposited events with debounced refetch - useWatchContractEvent({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - eventName: 'Deposited', - onLogs(logs) { - // Automatically refetch balance when deposit event is detected - refetchBalance() - - // Add deposit transactions to history - logs.forEach((log) => { - // The log.args shape from different providers may vary — coerce to unknown to - // avoid strict index/property type errors and normalize the values. - const args: unknown = log.args - const depositor = args?.depositor ?? args?.from ?? args?.[0] - const amount = args?.amount ?? args?.[1] - const timestamp = args?.timestamp ?? Date.now() - const newTransaction: Transaction = { - id: `${log.blockNumber}-${log.logIndex}`, - amount: Number(amount) / 1e18, // Convert from wei to ETH - timestamp: Number(timestamp), - type: 'deposit', - user: depositor as string, - } - setTransactions(prev => [newTransaction, ...prev].slice(0, 50)) // Keep last 50 transactions - }) - }, - }) - - // Watch for Withdrawn events with debounced refetch - useWatchContractEvent({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - eventName: 'Withdrawn', - onLogs(logs) { - // Automatically refetch balance when withdrawal event is detected - refetchBalance() - - // Add withdrawal transactions to history - logs.forEach((log) => { - const args: unknown = log.args - const withdrawer = args?.withdrawer ?? args?.to ?? args?.[0] - const amount = args?.amount ?? args?.[1] - const timestamp = args?.timestamp ?? Date.now() - const newTransaction: Transaction = { - id: `${log.blockNumber}-${log.logIndex}`, - amount: Number(amount) / 1e18, // Convert from wei to ETH - timestamp: Number(timestamp), - type: 'withdrawal', - user: withdrawer as string, - } - setTransactions(prev => [newTransaction, ...prev].slice(0, 50)) // Keep last 50 transactions - }) - }, - }) - - // Wait for transaction with memoization - const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ - hash, - }) - - // Memoize deposit function to prevent recreation on every render - const deposit = useCallback((amount: string) => { - if (!address) return - - writeContract({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - functionName: 'deposit', - value: parseEther(amount), - }) - }, [address, writeContract]) - - // Memoize withdraw function to prevent recreation on every render - const withdraw = useCallback(() => { - if (!address) return - - writeContract({ - address: PIGGYBANK_ADDRESS, - abi: PIGGYBANK_ABI, - functionName: 'withdraw', - }) - }, [address, writeContract]) - - // Withdraw all alias — forwards to owner withdraw - const withdrawAll = useCallback(() => { - withdraw() - }, [withdraw]) - - // Compute owner flag for convenience in components and tests - const isOwner = useMemo(() => { - return !!address && !!owner && address.toLowerCase() === owner.toLowerCase() - }, [address, owner]) - - // Memoize return object to prevent unnecessary re-renders - return useMemo(() => ({ - balance, - unlockTime, - owner, - transactions, - deposit, - withdraw, - withdrawAll, - isPending, - isConfirming, - isSuccess, - hash, - refetchBalance, - refetchUnlockTime, - isOwner, - debouncedRefetch, - }), [ - balance, - unlockTime, - owner, - transactions, - deposit, - withdraw, - withdrawAll, - isPending, - isConfirming, - isSuccess, - hash, - refetchBalance, - refetchUnlockTime, - isOwner, - debouncedRefetch - ]) -} diff --git a/frontend/src/utils/sharedValidation.ts b/frontend/src/utils/sharedValidation.ts index 7fccdb9..e69de29 100644 --- a/frontend/src/utils/sharedValidation.ts +++ b/frontend/src/utils/sharedValidation.ts @@ -1,217 +0,0 @@ -/** - * Shared validation utilities to eliminate code duplication - * Centralized validation functions used across multiple components - */ - -import { VALIDATION, REGEX_PATTERNS } from '../constants/appConstants'; - -/** - * Validates input length with configurable min/max - */ -export function validateInputLength( - input: string, - minLength: number = 1, - maxLength: number = VALIDATION.MAX_INPUT_LENGTH, - fieldName: string = 'Input' -): { isValid: boolean; error?: string } { - if (input.length === 0) { - return { isValid: false, error: `${fieldName} cannot be empty` }; - } - - if (input.length < minLength) { - return { isValid: false, error: `${fieldName} must be at least ${minLength} characters` }; - } - - if (input.length > maxLength) { - return { isValid: false, error: `${fieldName} must be less than ${maxLength} characters` }; - } - - return { isValid: true }; -} - -/** - * Validates numeric input with configurable min/max - */ -export function validateNumericInput( - input: string, - minValue: number = 0, - maxValue: number = VALIDATION.MAX_ETH_AMOUNT, - decimalPlaces: number = VALIDATION.MAX_DECIMAL_PLACES, - fieldName: string = 'Amount' -): { isValid: boolean; error?: string } { - const sanitized = input.trim(); - const amount = parseFloat(sanitized); - - if (isNaN(amount)) { - return { isValid: false, error: `${fieldName} must be a valid number` }; - } - - if (amount < minValue) { - return { isValid: false, error: `${fieldName} must be at least ${minValue}` }; - } - - if (amount > maxValue) { - return { isValid: false, error: `${fieldName} must be less than ${maxValue}` }; - } - - if (sanitized.includes('.') && sanitized.split('.')[1].length > decimalPlaces) { - return { isValid: false, error: `${fieldName} cannot have more than ${decimalPlaces} decimal places` }; - } - - return { isValid: true }; -} - -/** - * Validates Ethereum address format - */ -export function validateEthereumAddressFormat( - address: string, - fieldName: string = 'Address' -): { isValid: boolean; error?: string } { - const sanitized = address.trim(); - - if (!REGEX_PATTERNS.HEX_ADDRESS.test(sanitized)) { - return { isValid: false, error: `${fieldName} must be a valid Ethereum address` }; - } - - return { isValid: true }; -} - -/** - * Validates transaction hash format - */ -export function validateTransactionHashFormat( - hash: string, - fieldName: string = 'Transaction hash' -): { isValid: boolean; error?: string } { - const sanitized = hash.trim(); - - if (!REGEX_PATTERNS.TRANSACTION_HASH.test(sanitized)) { - return { isValid: false, error: `${fieldName} must be a valid transaction hash` }; - } - - return { isValid: true }; -} - -/** - * Validates timestamp range - */ -export function validateTimestampRange( - timestamp: number, - minTimestamp: number = VALIDATION.MIN_TIMESTAMP, - maxTimestamp: number = VALIDATION.MAX_TIMESTAMP, - fieldName: string = 'Timestamp' -): { isValid: boolean; error?: string } { - if (isNaN(timestamp)) { - return { isValid: false, error: `${fieldName} must be a valid number` }; - } - - if (timestamp < minTimestamp || timestamp > maxTimestamp) { - return { isValid: false, error: `${fieldName} must be between ${new Date(minTimestamp * 1000).getFullYear()} and ${new Date(maxTimestamp * 1000).getFullYear()}` }; - } - - return { isValid: true }; -} - -/** - * Sanitizes string input to prevent XSS - */ -export function sanitizeStringInput( - input: string, - maxLength: number = VALIDATION.MAX_INPUT_LENGTH -): string { - if (typeof input !== 'string') return ''; - - return input - .trim() - .replace(/[<>'"&]/g, '') - .substring(0, maxLength); -} - -/** - * Validates array size - */ -export function validateArraySize( - array: any[], - maxSize: number = VALIDATION.MAX_ARRAY_SIZE, - fieldName: string = 'Array' -): { isValid: boolean; error?: string } { - if (array.length > maxSize) { - return { isValid: false, error: `${fieldName} cannot exceed ${maxSize} items` }; - } - - return { isValid: true }; -} - -/** - * Validates storage key format - */ -export function validateStorageKey( - key: string, - maxLength: number = VALIDATION.MAX_STORAGE_KEY_LENGTH -): { isValid: boolean; error?: string } { - if (!/^[a-zA-Z0-9._-]+$/.test(key)) { - return { isValid: false, error: 'Storage key contains invalid characters' }; - } - - if (key.length > maxLength) { - return { isValid: false, error: `Storage key cannot exceed ${maxLength} characters` }; - } - - return { isValid: true }; -} - -/** - * Creates a validation schema for complex objects - */ -export function createValidationSchema>( - data: T, - rules: Partial { isValid: boolean; error?: string }>> -): { isValid: boolean; data: T; errors: Record } { - const errors: Record = {}; - const validatedData = { ...data }; - - for (const [key, rule] of Object.entries(rules)) { - if (rule) { - const result = rule(data[key as keyof T]); - if (!result.isValid) { - errors[key] = result.error || 'Validation failed'; - } else { - validatedData[key as keyof T] = data[key as keyof T]; - } - } - } - - return { - isValid: Object.keys(errors).length === 0, - data: validatedData, - errors - }; -} - -/** - * Common validation patterns - */ -export const COMMON_VALIDATION_PATTERNS = { - ETH_AMOUNT: REGEX_PATTERNS.ETH_AMOUNT, - HEX_ADDRESS: REGEX_PATTERNS.HEX_ADDRESS, - TRANSACTION_HASH: REGEX_PATTERNS.TRANSACTION_HASH, - NUMBER: REGEX_PATTERNS.NUMBER, - EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - URL: /^https?:\/\/[^\s/$.?#].[^\s]*$/i, -} as const; - -/** - * Error messages for consistent UX - */ -export const COMMON_ERROR_MESSAGES = { - REQUIRED: 'This field is required', - INVALID_FORMAT: 'Invalid format', - TOO_LONG: 'Input is too long', - TOO_SHORT: 'Input is too short', - INVALID_ETH_AMOUNT: 'Please enter a valid ETH amount', - INVALID_ADDRESS: 'Please enter a valid Ethereum address', - INVALID_HASH: 'Please enter a valid transaction hash', - INVALID_EMAIL: 'Please enter a valid email address', - INVALID_URL: 'Please enter a valid URL', -} as const; \ No newline at end of file diff --git a/frontend/src/utils/validateEnv.test.ts b/frontend/src/utils/validateEnv.test.ts index 18d1043..e69de29 100644 --- a/frontend/src/utils/validateEnv.test.ts +++ b/frontend/src/utils/validateEnv.test.ts @@ -1,884 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { validateEnvironment, getEnvironmentInfo } from './validateEnv' - -// Mock import.meta.env with more comprehensive structure -const mockImportMeta = { - env: { - VITE_REOWN_PROJECT_ID: '', - VITE_PIGGYBANK_ADDRESS: '', - VITE_BUILD_TIMESTAMP: new Date().toISOString(), - VITE_APP_VERSION: '1.0.0', - PROD: false, - DEV: true, - MODE: 'development' - } -} - -// Mock process.env with more realistic values -const originalProcessEnv = { ...process.env } - -// Enhanced mock console with call tracking -const mockConsole = { - error: vi.fn(), - warn: vi.fn(), - log: vi.fn(), - info: vi.fn(), - debug: vi.fn() -} - -// Setup enhanced mocks before each test -beforeEach(() => { - // Reset all mocks - vi.clearAllMocks() - - // Enhanced console mocking with call tracking - vi.spyOn(console, 'error').mockImplementation(mockConsole.error) - vi.spyOn(console, 'warn').mockImplementation(mockConsole.warn) - vi.spyOn(console, 'log').mockImplementation(mockConsole.log) - vi.spyOn(console, 'info').mockImplementation(mockConsole.info) - vi.spyOn(console, 'debug').mockImplementation(mockConsole.debug) - - // Mock import.meta.env with comprehensive structure - Object.defineProperty(globalThis, 'import.meta', { - value: mockImportMeta, - writable: true, - configurable: true - }) - - // Mock process.env for CI environment variables with more realistic setup - Object.keys(originalProcessEnv).forEach(key => { - if (key.startsWith('CI') || key.includes('GITHUB') || key.includes('GITLAB') || - key.includes('CIRCLE') || key.includes('TRAVIS') || key.includes('JENKINS')) { - delete process.env[key] - } - }) - - // Mock additional environment variables that might be used - process.env.NODE_ENV = 'development' - process.env.PUBLIC_URL = '/' -}) - -afterEach(() => { - // Restore console methods - vi.restoreAllMocks() - - // Restore process.env - Object.keys(process.env).forEach(key => { - if (key.startsWith('CI') || key.includes('GITHUB') || key.includes('GITLAB') || - key.includes('CIRCLE') || key.includes('TRAVIS') || key.includes('JENKINS')) { - delete process.env[key] - } - }) - - // Reset import.meta.env to default values - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.VITE_BUILD_TIMESTAMP = new Date().toISOString() - mockImportMeta.env.VITE_APP_VERSION = '1.0.0' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = true - mockImportMeta.env.MODE = 'development' - - // Restore original process.env - Object.assign(process.env, originalProcessEnv) -}) - -describe('validateEnvironment', () => { - describe('Project ID Validation', () => { - it('should return error when project ID is missing', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_REOWN_PROJECT_ID is not set. Get one from https://cloud.reown.com/ This is required for wallet connection functionality.') - expect(result.warnings).toHaveLength(0) - }) - - it('should return warning when project ID is too short', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'short' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) // Still valid, just a warning - expect(result.warnings).toContain('VITE_REOWN_PROJECT_ID seems too short. Verify it is correct.') - expect(result.errors).toHaveLength(0) - }) - - it('should pass validation with valid project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' // 32 chars - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should pass validation with long valid project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012345678901234567890' // 44 chars - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - }) - - describe('Contract Address Validation', () => { - it('should return error when contract address is missing in development', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) // In development, missing contract address is a warning - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(1) - expect(result.warnings[0]).toContain('VITE_PIGGYBANK_ADDRESS is not set') - expect(result.warnings[0]).toContain('This will be an error in CI/production builds') - }) - - it('should return error when contract address does not start with 0x', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS must start with "0x"') - expect(result.warnings).toHaveLength(0) - }) - - it('should return error when contract address has wrong length', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x12345678901234567890123456789012' // 34 chars instead of 42 - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS must be 42 characters (including "0x")') - expect(result.warnings).toHaveLength(0) - }) - - it('should pass validation with valid contract address', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should accept exactly 42 character contract address', () => { - const validAddress = '0x' + 'a'.repeat(40) - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = validAddress - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - }) - - describe('Strict Validation Logic', () => { - it('should enable strict validation in CI environment', () => { - // Set various CI environment variables - process.env.CI = 'true' - process.env.GITHUB_ACTIONS = 'true' - - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'test' - - const result = validateEnvironment() - - expect(result.isStrict).toBe(true) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS is not set') - expect(result.warnings).toHaveLength(0) - }) - - it('should enable strict validation in production mode', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = true - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'production' - - const result = validateEnvironment() - - expect(result.isStrict).toBe(true) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS is not set') - expect(result.warnings).toHaveLength(0) - }) - - it('should disable strict validation in development mode', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = true - mockImportMeta.env.MODE = 'development' - - const result = validateEnvironment() - - expect(result.isStrict).toBe(false) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(1) - expect(result.warnings[0]).toContain('This will be an error in CI/production builds') - }) - - it('should handle different CI environment variables', () => { - const ciVars = [ - { key: 'GITLAB_CI', value: 'true' }, - { key: 'CIRCLECI', value: 'true' }, - { key: 'TRAVIS', value: 'true' }, - { key: 'JENKINS_URL', value: 'http://jenkins.example.com' } - ] - - ciVars.forEach(({ key, value }) => { - process.env[key] = value - - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'test' - - const result = validateEnvironment() - - expect(result.isStrict).toBe(true) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS is not set') - - delete process.env[key] - }) - }) - }) - - describe('Error and Warning Paths', () => { - it('should handle multiple errors and warnings together', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'short' // Too short - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' // Missing - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toHaveLength(0) // No errors in development - expect(result.warnings).toHaveLength(2) // Project ID warning + Contract address warning - }) - - it('should handle multiple errors in strict mode', () => { - process.env.CI = 'true' - - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'short' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' // Missing - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'test' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors.length).toBeGreaterThan(0) - expect(result.warnings.length).toBeGreaterThanOrEqual(0) - }) - - it('should return valid result when no issues found', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should call console methods when errors exist', () => { - const consoleErrorSpy = vi.spyOn(console, 'error') - const consoleWarnSpy = vi.spyOn(console, 'warn') - const consoleLogSpy = vi.spyOn(console, 'log') - - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - validateEnvironment() - - expect(consoleErrorSpy).toHaveBeenCalled() - expect(consoleWarnSpy).not.toHaveBeenCalled() - expect(consoleLogSpy).not.toHaveBeenCalled() - }) - - it('should call console methods when warnings exist', () => { - const consoleErrorSpy = vi.spyOn(console, 'error') - const consoleWarnSpy = vi.spyOn(console, 'warn') - const consoleLogSpy = vi.spyOn(console, 'log') - - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - validateEnvironment() - - expect(consoleErrorSpy).not.toHaveBeenCalled() - expect(consoleWarnSpy).toHaveBeenCalled() - expect(consoleLogSpy).not.toHaveBeenCalled() - }) - - it('should call console.log when everything is valid', () => { - const consoleErrorSpy = vi.spyOn(console, 'error') - const consoleWarnSpy = vi.spyOn(console, 'warn') - const consoleLogSpy = vi.spyOn(console, 'log') - - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - validateEnvironment() - - expect(consoleErrorSpy).not.toHaveBeenCalled() - expect(consoleWarnSpy).not.toHaveBeenCalled() - expect(consoleLogSpy).toHaveBeenCalled() - }) - }) - - describe('getEnvironmentInfo', () => { - it('should return environment info with valid values', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = true - mockImportMeta.env.MODE = 'development' - - const info = getEnvironmentInfo() - - expect(info.projectId).toBe('✅ Set') - expect(info.contractAddress).toBe('✅ Set') - expect(info.mode).toBe('development') - expect(info.isDevelopment).toBe(true) - expect(info.isProduction).toBe(false) - }) - - it('should return environment info with missing values', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = true - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'production' - - const info = getEnvironmentInfo() - - expect(info.projectId).toBe('❌ Missing') - expect(info.contractAddress).toBe('⚠️ Not Set') - expect(info.mode).toBe('production') - expect(info.isDevelopment).toBe(false) - expect(info.isProduction).toBe(true) - }) - - it('should return correct info for contract address missing but project ID present', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = true - mockImportMeta.env.MODE = 'development' - - const info = getEnvironmentInfo() - - expect(info.projectId).toBe('✅ Set') - expect(info.contractAddress).toBe('⚠️ Not Set') - }) - }) - - describe('Edge Cases', () => { - it('should handle null/undefined project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = null as unknown - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - expect(() => validateEnvironment()).not.toThrow() - const result = validateEnvironment() - expect(result.errors.some(error => error.includes('VITE_REOWN_PROJECT_ID'))).toBe(true) - }) - - it('should handle null/undefined contract address', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = null as any - - expect(() => validateEnvironment()).not.toThrow() - const result = validateEnvironment() - expect(result.errors.some(error => error.includes('VITE_PIGGYBANK_ADDRESS'))).toBe(true) - }) - - it('should handle undefined project ID explicitly', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockImportMeta.env.VITE_REOWN_PROJECT_ID = undefined as any - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_REOWN_PROJECT_ID is not set. Get one from https://cloud.reown.com/ This is required for wallet connection functionality.') - }) - - it('should handle undefined contract address explicitly', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = undefined as any - - const result = validateEnvironment() - expect(result.isValid).toBe(true) // In development, missing is a warning - expect(result.warnings).toHaveLength(1) - expect(result.warnings[0]).toContain('VITE_PIGGYBANK_ADDRESS is not set') - }) - - it('should handle project ID with exactly 32 characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(32) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle project ID with exactly 31 characters (should warn)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(31) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.warnings).toContain('VITE_REOWN_PROJECT_ID seems too short. Verify it is correct.') - }) - - it('should handle empty string project ID (should error)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_REOWN_PROJECT_ID is not set') - }) - - it('should handle whitespace-only project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = ' ' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - // Whitespace-only should be treated as invalid/too short - expect(result.isValid).toBe(false) - expect(result.errors.some(error => error.includes('VITE_REOWN_PROJECT_ID'))).toBe(true) - }) - - it('should handle contract address with leading/trailing whitespace', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = ' 0x1234567890123456789012345678901234567890 ' - - const result = validateEnvironment() - - // Should fail validation due to whitespace - expect(result.isValid).toBe(false) - expect(result.errors.some(error => error.includes('VITE_PIGGYBANK_ADDRESS must start with "0x"'))).toBe(true) - }) - - it('should handle very long contract address', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890abcdefghijklmnop' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS must be 42 characters (including "0x")') - }) - - it('should handle project ID with special characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'abc123-xyz_789.GHI+jkl=' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - // Should pass validation as it's long enough (> 32 chars) - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle contract address with mixed case hex characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0xAbCdEf1234567890123456789012345678901234' - - const result = validateEnvironment() - - // Should pass validation - mixed case is allowed - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - - it('should handle project ID with exactly 33 characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(33) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - // Should pass validation (> 32 chars) - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle contract address with all uppercase hex after 0x', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0xABCDEF1234567890123456789012345678901234' - - const result = validateEnvironment() - - // Should pass validation - uppercase hex is valid - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - - it('should handle contract address with all lowercase hex after 0x', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0xabcdef1234567890123456789012345678901234' - - const result = validateEnvironment() - - // Should pass validation - lowercase hex is valid - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - }) - - it('should handle very short project ID (1 character)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'x' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - // Should show warning for being too short - expect(result.isValid).toBe(true) - expect(result.warnings).toContain('VITE_REOWN_PROJECT_ID seems too short. Verify it is correct.') - expect(result.errors).toHaveLength(0) - }) - - it('should handle very long project ID (100 characters)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(100) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - // Should pass validation - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - }) - - describe('Environment Variable Combinations', () => { - it('should handle all variables missing in development', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) // Missing project ID - expect(result.errors).toContain('VITE_REOWN_PROJECT_ID is not set') - expect(result.warnings).toHaveLength(1) - }) - - it('should handle all variables missing in CI', () => { - process.env.CI = 'true' - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'test' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors.length).toBeGreaterThanOrEqual(2) // Both should be errors in CI - expect(result.warnings).toHaveLength(0) - }) - - it('should handle CI=true with different values', () => { - const ciValues = ['true', '1', 'True', 'TRUE'] - - ciValues.forEach(ciValue => { - process.env.CI = ciValue - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'test' - - const result = validateEnvironment() - - expect(result.isStrict).toBe(true) - expect(result.errors.length).toBeGreaterThanOrEqual(2) - - delete process.env.CI - }) - }) - - it('should handle valid project ID with invalid contract address', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = 'invalid-address' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS must start with "0x"') - }) - - it('should handle invalid project ID with valid contract address', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'short' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) // Just a warning - expect(result.warnings).toContain('VITE_REOWN_PROJECT_ID seems too short. Verify it is correct.') - expect(result.errors).toHaveLength(0) - }) - - it('should validate exact error message for missing project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.errors).toContain('VITE_REOWN_PROJECT_ID is not set. Get one from https://cloud.reown.com/ This is required for wallet connection functionality.') - expect(result.errors.length).toBe(1) - }) - - it('should validate exact error message for missing contract address in CI', () => { - process.env.CI = 'true' - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'test' - - const result = validateEnvironment() - - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS is not set. The application cannot interact with the smart contract without this address. Deploy your contract and set this variable in your .env file.') - expect(result.warnings).toHaveLength(0) - }) - - it('should validate exact warning message for missing contract address in development', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - const result = validateEnvironment() - - expect(result.warnings).toContain('VITE_PIGGYBANK_ADDRESS is not set. The application cannot interact with the smart contract without this address. Deploy your contract and set this variable in your .env file. (This will be an error in CI/production builds)') - }) - - it('should handle contract address with exactly 41 characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x' + 'a'.repeat(39) // 41 total characters - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS must be 42 characters (including "0x")') - }) - - it('should handle contract address with exactly 43 characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x' + 'a'.repeat(41) // 43 total characters - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS must be 42 characters (including "0x")') - }) - - it('should handle numeric project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should test strict validation logic with non-dev, non-production environment', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = false - mockImportMeta.env.MODE = 'staging' - - const result = validateEnvironment() - - // Should be strict validation when not development - expect(result.isStrict).toBe(true) - expect(result.errors.length).toBeGreaterThanOrEqual(2) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle environment where only CI variable is set', () => { - process.env.GITHUB_ACTIONS = 'true' - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - mockImportMeta.env.PROD = false - mockImportMeta.env.DEV = true - mockImportMeta.env.MODE = 'development' - - const result = validateEnvironment() - - // GitHub Actions should override development mode and enable strict validation - expect(result.isStrict).toBe(true) - expect(result.errors).toContain('VITE_PIGGYBANK_ADDRESS is not set') - expect(result.warnings).toHaveLength(0) - - delete process.env.GITHUB_ACTIONS - }) - }) - - describe('Additional Validation Scenarios', () => { - it('should handle project ID with exactly 32 characters (minimum valid length)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(32) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle project ID with exactly 31 characters (should warn)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(31) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.warnings).toContain('VITE_REOWN_PROJECT_ID seems too short. Verify it is correct.') - expect(result.errors).toHaveLength(0) - }) - - it('should handle contract address with mixed case hex characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0xAbCdEf1234567890123456789012345678901234' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle contract address with all uppercase hex after 0x', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0xABCDEF1234567890123456789012345678901234' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle contract address with all lowercase hex after 0x', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0xabcdef1234567890123456789012345678901234' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - }) - - describe('Performance and Edge Cases', () => { - it('should handle very long project ID (100 characters)', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'a'.repeat(100) - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle project ID with special characters', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = 'abc123-xyz_789.GHI+jkl=' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(true) - expect(result.errors).toHaveLength(0) - expect(result.warnings).toHaveLength(0) - }) - - it('should handle whitespace-only project ID', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = ' ' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors.some(error => error.includes('VITE_REOWN_PROJECT_ID'))).toBe(true) - }) - - it('should handle contract address with leading/trailing whitespace', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = ' 0x1234567890123456789012345678901234567890 ' - - const result = validateEnvironment() - - expect(result.isValid).toBe(false) - expect(result.errors.some(error => error.includes('VITE_PIGGYBANK_ADDRESS must start with "0x"'))).toBe(true) - }) - }) - - describe('Console Output Verification', () => { - it('should call console.error when errors exist', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - const result = validateEnvironment() - - expect(mockConsole.error).toHaveBeenCalled() - expect(mockConsole.warn).not.toHaveBeenCalled() - expect(mockConsole.log).not.toHaveBeenCalled() - }) - - it('should call console.warn when warnings exist', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '' - - const result = validateEnvironment() - - expect(mockConsole.error).not.toHaveBeenCalled() - expect(mockConsole.warn).toHaveBeenCalled() - expect(mockConsole.log).not.toHaveBeenCalled() - }) - - it('should call console.log when everything is valid', () => { - mockImportMeta.env.VITE_REOWN_PROJECT_ID = '12345678901234567890123456789012' - mockImportMeta.env.VITE_PIGGYBANK_ADDRESS = '0x1234567890123456789012345678901234567890' - - const result = validateEnvironment() - - expect(mockConsole.error).not.toHaveBeenCalled() - expect(mockConsole.warn).not.toHaveBeenCalled() - expect(mockConsole.log).toHaveBeenCalled() - }) - }) -}) \ No newline at end of file diff --git a/frontend/src/utils/validateEnv.ts b/frontend/src/utils/validateEnv.ts index 61188d5..e69de29 100644 --- a/frontend/src/utils/validateEnv.ts +++ b/frontend/src/utils/validateEnv.ts @@ -1,102 +0,0 @@ -// Environment Variable Validation Utility - -/** - * Determines if the current environment requires strict validation - * Strict validation is enabled for CI, production builds, and non-dev environments - */ -function isStrictValidationEnabled(): boolean { - // Check if running in CI environment - const isCI = Boolean( - process.env.CI || - process.env.GITHUB_ACTIONS || - process.env.GITLAB_CI || - process.env.CIRCLECI || - process.env.TRAVIS || - process.env.JENKINS_URL - ) - - // Strict validation in production build or CI - const isProduction = import.meta.env.PROD - const isDevelopment = import.meta.env.DEV - - return isCI || isProduction || !isDevelopment -} - -export function validateEnvironment() { - const errors: string[] = [] - const warnings: string[] = [] - const isStrict = isStrictValidationEnabled() - - // Check REOWN Project ID - const projectId = import.meta.env.VITE_REOWN_PROJECT_ID - if (!projectId) { - errors.push( - 'VITE_REOWN_PROJECT_ID is not set. ' + - 'Get one from https://cloud.reown.com/ ' + - 'This is required for wallet connection functionality.' - ) - } else if (projectId.length < 32) { - warnings.push('VITE_REOWN_PROJECT_ID seems too short. Verify it is correct.') - } - - // Check PiggyBank Address - strict validation in CI/production - const contractAddress = import.meta.env.VITE_PIGGYBANK_ADDRESS - if (!contractAddress) { - const message = - 'VITE_PIGGYBANK_ADDRESS is not set. ' + - 'The application cannot interact with the smart contract without this address. ' + - 'Deploy your contract and set this variable in your .env file.' - - if (isStrict) { - // Error in CI/production - this should block builds - errors.push(message) - } else { - // Warning in local dev - allows developers to work on UI without contract - warnings.push(message + ' (This will be an error in CI/production builds)') - } - } else if (!contractAddress.startsWith('0x')) { - errors.push('VITE_PIGGYBANK_ADDRESS must start with "0x"') - } else if (contractAddress.length !== 42) { - errors.push('VITE_PIGGYBANK_ADDRESS must be 42 characters (including "0x")') - } - - // Log results - if (errors.length > 0) { - console.error('❌ Environment Configuration Errors:') - errors.forEach(error => console.error(` - ${error}`)) - - if (isStrict) { - console.error('\n🚫 Build cannot proceed with missing required environment variables.') - console.error('Please check your .env file or CI/CD environment variable configuration.') - } - } - - if (warnings.length > 0) { - console.warn('⚠️ Environment Configuration Warnings:') - warnings.forEach(warning => console.warn(` - ${warning}`)) - } - - if (errors.length === 0 && warnings.length === 0) { - // Only log success in development mode - if (import.meta.env.DEV) { - console.log('✅ Environment configuration is valid') - } - } - - return { - isValid: errors.length === 0, - errors, - warnings, - isStrict - } -} - -export function getEnvironmentInfo() { - return { - projectId: import.meta.env.VITE_REOWN_PROJECT_ID ? '✅ Set' : '❌ Missing', - contractAddress: import.meta.env.VITE_PIGGYBANK_ADDRESS ? '✅ Set' : '⚠️ Not Set', - mode: import.meta.env.MODE, - isDevelopment: import.meta.env.DEV, - isProduction: import.meta.env.PROD - } -} diff --git a/frontend/vitest.config b/frontend/vitest.config deleted file mode 100644 index 99f79f7..0000000 --- a/frontend/vitest.config +++ /dev/null @@ -1,28 +0,0 @@ -import { defineConfig } from 'vitest/config' -import react from '@vitejs/plugin-react' -import path from 'path' - -export default defineConfig({ - plugins: [react()], - test: { - globals: true, - environment: 'jsdom', - setupFiles: ['./src/test/setup.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], - exclude: [ - 'node_modules/', - 'src/test/', - '**/*.test.{ts,tsx}', - '**/*.config.{ts,js}', - '**/types.ts', - ], - }, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, -}) \ No newline at end of file