diff --git a/contracts/lib/self b/contracts/lib/self new file mode 160000 index 0000000..03e471c --- /dev/null +++ b/contracts/lib/self @@ -0,0 +1 @@ +Subproject commit 03e471c5bfa5a6dfe5e69324cbf060f1545cf679 diff --git a/next-frontend/.github/workflows/e2e-tests.yml b/next-frontend/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..fe00683 --- /dev/null +++ b/next-frontend/.github/workflows/e2e-tests.yml @@ -0,0 +1,211 @@ +name: E2E Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + browser: [chromium, firefox, webkit] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps ${{ matrix.browser }} + + - name: Build application + run: npm run build + env: + NEXT_PUBLIC_TEST_MODE: true + NEXT_PUBLIC_MOCK_CONTRACTS: true + + - name: Run Playwright tests + run: npx playwright test --project=${{ matrix.browser }} + env: + CI: true + NEXT_PUBLIC_TEST_MODE: true + NEXT_PUBLIC_E2E_TESTING: true + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-results-${{ matrix.browser }}-node-${{ matrix.node-version }} + path: | + test-results/ + playwright-report/ + playbackwright-report/ + retention-days: 30 + + security-tests: + runs-on: ubuntu-latest + needs: e2e-tests + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run security-focused E2E tests + run: npx playwright test tests/security/ + env: + CI: true + NEXT_PUBLIC_TEST_MODE: true + + performance-tests: + runs-on: ubuntu-latest + needs: e2e-tests + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run performance E2E tests + run: npx playwright test --project=chromium --grep="performance|load" + env: + CI: true + NEXT_PUBLIC_TEST_MODE: true + + - name: Upload performance results + uses: actions/upload-artifact@v4 + if: always() + with: + name: performance-results + path: | + test-results/ + playwright-report/ + retention-days: 7 + + mobile-tests: + runs-on: ubuntu-latest + needs: e2e-tests + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run mobile E2E tests + run: npx playwright test --project="Mobile Chrome" --project="Mobile Safari" + env: + CI: true + NEXT_PUBLIC_TEST_MODE: true + + cross-browser-tests: + runs-on: ubuntu-latest + needs: e2e-tests + + strategy: + matrix: + browser: [chromium, firefox, webkit] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps ${{ matrix.browser }} + + - name: Run cross-browser compatibility tests + run: npx playwright test --project=${{ matrix.browser }} --grep="cross-browser|compatibility" + env: + CI: true + NEXT_PUBLIC_TEST_MODE: true + + test-summary: + runs-on: ubuntu-latest + needs: [e2e-tests, security-tests, performance-tests, mobile-tests, cross-browser-tests] + if: always() + + steps: + - name: Download all test results + uses: actions/download-artifact@v4 + with: + path: all-results/ + + - name: Generate test summary + run: | + echo "# E2E Test Results Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if tests passed + if [ -f "all-results/playwright-report/index.html" ]; then + echo "โœ… E2E tests completed successfully" >> $GITHUB_STEP_SUMMARY + echo "- Cross-browser testing passed" >> $GITHUB_STEP_SUMMARY + echo "- Security tests completed" >> $GITHUB_STEP_SUMMARY + echo "- Performance tests executed" >> $GITHUB_STEP_SUMMARY + echo "- Mobile compatibility verified" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ E2E tests failed" >> $GITHUB_STEP_SUMMARY + echo "Please check the test artifacts for detailed failure information." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Test Artifacts" >> $GITHUB_STEP_SUMMARY + echo "- [Playwright Report](./artifacts)" >> $GITHUB_STEP_SUMMARY + echo "- [Test Results](./test-results/)" >> $GITHUB_STEP_SUMMARY + + - name: Upload all artifacts + uses: actions/upload-artifact@v4 + with: + name: all-test-results + path: all-results/ + retention-days: 30 \ No newline at end of file diff --git a/next-frontend/ENS_INTEGRATION_SUMMARY.md b/next-frontend/ENS_INTEGRATION_SUMMARY.md deleted file mode 100644 index d07ef3b..0000000 --- a/next-frontend/ENS_INTEGRATION_SUMMARY.md +++ /dev/null @@ -1,214 +0,0 @@ -# ENS Profile Integration - Implementation Summary - -## Overview -This pull request implements ENS (Ethereum Name Service) user profile display functionality for BlockBelle's chat interface. The implementation provides a comprehensive solution for fetching, displaying, and managing ENS profiles with graceful fallbacks and real-time updates. - -## ๐ŸŽฏ Features Implemented - -### โœ… Core Functionality -- **ENS Profile Fetching**: Retrieve comprehensive profile data including avatar, bio, website, Twitter, and GitHub -- **Avatar Support**: Handle IPFS URLs and automatic conversion to accessible URLs -- **Profile Display**: Show ENS names, display names, bios, and verification badges -- **Fallback Handling**: Graceful degradation to default profiles when ENS data is unavailable -- **Real-time Updates**: Monitor and update profiles when ENS records change - -### โœ… User Interface Integration -- **UserList Enhancement**: Updated user list to display ENS profiles with avatars and verification -- **Chat Interface**: Integrated ENS profiles in chat headers and message displays -- **Notification System**: Updated to use ENS display names for better user experience -- **Multiple Display Modes**: Support for compact and full profile views - -### โœ… Technical Implementation -- **ENS Service Layer**: Comprehensive service for profile management and caching -- **React Hooks**: Custom hooks for individual and batch profile management -- **Component Library**: Reusable ENS profile components with multiple size options -- **Change Detection**: Smart polling mechanism for detecting profile updates - -## ๐Ÿ“ Files Modified/Created - -### New Files -- `src/lib/ensService.ts` - Core ENS service with fetching, caching, and subscription logic -- `src/hooks/useENSProfile.ts` - React hooks for profile management -- `src/components/ENSProfile.tsx` - Reusable ENS profile component -- `src/__tests__/ens.test.ts` - Comprehensive service layer tests -- `src/__tests__/ens-hooks.test.tsx` - React hooks test suite -- `src/__tests__/ens-profile-component.test.tsx` - Component integration tests - -### Modified Files -- `package.json` - Added ENS SDK dependencies -- `src/components/UserList.tsx` - Integrated ENS profile display -- `src/components/ChatInterface.tsx` - Added ENS profile integration in chat - -## ๐Ÿ”ง Technical Details - -### ENS Service Features -```typescript -// Key service functions implemented: -- fetchENSProfile(address) - Fetch individual profile -- fetchMultipleENSProfiles(addresses[]) - Batch profile fetching -- subscribeToENSChanges(address, callback) - Real-time updates -- subscribeToMultipleENSChanges() - Batch subscription management -- detectProfileChanges(address) - Change detection with field comparison -- Cache management with TTL and invalidation -``` - -### React Hooks -```typescript -// Hooks implemented: -- useENSProfile(address) - Individual profile management -- useENSProfiles(addresses[]) - Batch profile management -- useENSDisplayInfo(address) - Formatted display data -``` - -### Component Features -```typescript -// ENSProfile component props: -- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' -- showBadge: boolean -- showFullProfile: boolean -- fallbackToAddress: boolean -- Custom className support -``` - -## ๐Ÿงช Testing Coverage - -### Service Layer Tests -- Profile fetching with mock data -- Cache management (get/set/clear) -- Subscription lifecycle -- Error handling and edge cases -- Performance testing with batch operations - -### Hook Tests -- Individual and batch profile fetching -- Loading states and error handling -- Integration with ENS service -- Cleanup and subscription management - -### Component Tests -- Rendering with various profile states -- Avatar handling and fallback behavior -- Social link integration -- Size and prop variations -- External link security - -## ๐ŸŽจ UI/UX Enhancements - -### Profile Display -- **Avatar System**: ENS avatars with fallback to initials -- **Verification Badges**: Visual indicators for verified users -- **Bio Integration**: Display user bios when available -- **Social Links**: Direct links to website, Twitter, and GitHub -- **ENS Badges**: Special indicators for users with ENS profiles - -### User Experience -- **Loading States**: Skeleton screens during profile fetch -- **Error Handling**: Graceful fallbacks for missing data -- **Real-time Updates**: Automatic profile updates -- **Responsive Design**: Works across all device sizes - -## ๐Ÿš€ Performance Optimizations - -### Caching Strategy -- **In-memory Cache**: 5-minute TTL for profile data -- **Batch Operations**: Process multiple addresses efficiently -- **Lazy Loading**: Load profiles on demand -- **Subscription Cleanup**: Proper cleanup to prevent memory leaks - -### Network Optimization -- **Batch Fetching**: Process up to 5 addresses simultaneously -- **Error Recovery**: Retry mechanisms for failed requests -- **Minimal Re-renders**: Optimized React hooks for performance - -## ๐Ÿ”’ Security & Privacy - -### Data Handling -- **Client-side Only**: All profile fetching happens client-side -- **No Data Storage**: Profiles not stored in database -- **External Links**: Proper `rel="noopener noreferrer"` attributes -- **Input Validation**: Address validation and sanitization - -## ๐Ÿ“ฑ Mobile Responsiveness - -### Responsive Design -- **Adaptive Sizing**: Profile components scale appropriately -- **Touch-Friendly**: Proper touch targets for mobile devices -- **Collapsible Info**: Bio and social links adapt to screen size -- **Loading States**: Mobile-optimized loading indicators - -## ๐Ÿ”ฎ Future Enhancements - -### Ready for Production -- **ENS SDK Integration**: Mock implementation ready for real ENS SDK -- **Database Integration**: Easy transition to persistent storage -- **Real-time Subscriptions**: Foundation for WebSocket-based updates -- **Advanced Caching**: Redis integration ready - -### Potential Additions -- **Profile Editing**: Allow users to update their ENS records -- **Bulk Operations**: Admin tools for managing multiple profiles -- **Analytics**: Track profile usage and performance metrics -- **Social Features**: Follow/block users based on ENS data - -## ๐Ÿ“‹ Acceptance Criteria Met - -### โœ… Fetch and display ENS avatar and profile for authenticated users -- Implemented comprehensive profile fetching service -- Created reusable React components with avatar support -- Integrated into chat interface and user list - -### โœ… Fallback gracefully to default profile details if ENS data is incomplete -- Default profiles for addresses without ENS -- Fallback to initials when avatars fail to load -- Error handling with user-friendly messages - -### โœ… Ensure profiles are updated if ENS records change -- Real-time monitoring with configurable polling intervals -- Automatic UI updates when profile data changes -- Smart change detection with field comparison - -### โœ… Add tests to verify ENS profile integration -- Comprehensive test suite covering all components -- Service layer testing with mock data -- React component integration testing -- Edge case and error handling tests - -## ๐Ÿ› ๏ธ Technical Stack - -### Dependencies -- **@ensdomains/ensjs** - ENS SDK for profile operations -- **@ensdomains/ens-contracts** - ENS contract interfaces -- **React 19** - Modern React with concurrent features -- **Wagmi** - Ethereum interaction library -- **TypeScript** - Type-safe development - -### Development Tools -- **Vitest** - Fast unit testing framework -- **Testing Library** - React component testing utilities -- **msw** - API mocking for tests - -## ๐Ÿ“Š Code Quality - -### Metrics -- **Test Coverage**: 95%+ across all modules -- **Type Safety**: 100% TypeScript coverage -- **Documentation**: Comprehensive inline documentation -- **Error Handling**: Robust error boundaries and fallbacks - -### Architecture -- **Separation of Concerns**: Service layer, hooks, and components -- **Reusability**: Modular design for easy extension -- **Performance**: Optimized for real-world usage patterns -- **Maintainability**: Clean, well-documented code structure - -## ๐ŸŽ‰ Summary - -This implementation successfully delivers a production-ready ENS profile integration for BlockBelle's chat interface. The solution provides: - -1. **Complete Feature Set**: All requirements implemented with additional enhancements -2. **Robust Testing**: Comprehensive test suite ensuring reliability -3. **Performance Optimized**: Efficient caching and batch operations -4. **User-Friendly**: Intuitive interface with proper fallbacks -5. **Future-Ready**: Extensible architecture for additional features - -The implementation is ready for production deployment and can be easily extended with additional ENS features or migrated to use the real ENS SDK when dependencies are available. \ No newline at end of file diff --git a/next-frontend/I18N_CONTRIBUTING.md b/next-frontend/I18N_CONTRIBUTING.md deleted file mode 100644 index 0e32bf0..0000000 --- a/next-frontend/I18N_CONTRIBUTING.md +++ /dev/null @@ -1,422 +0,0 @@ -# Internationalization Contributing Guide - -This guide helps developers contribute to BlockBelle's internationalization (i18n) system. - -## Quick Start for Developers - -### Adding New Text - -When you add new text to the application, **always use translation keys**: - -```typescript -// โŒ Don't do this -

Welcome to BlockBelle

- -// โœ… Always do this -

{t('homepage.welcome')}

-``` - -### Before You Start Development - -1. **Install dependencies**: `npm install` (this will install `next-intl`) -2. **Check existing translations**: Look at `src/i18n/locales/` for reference -3. **Understand the structure**: Review `INTERNATIONALIZATION.md` - -## Translation Workflow - -### Step 1: Identify Translation Needs - -When adding new features or text: - -1. **Scan for hardcoded strings** in your new components -2. **Identify text types**: Buttons, labels, error messages, notifications -3. **Plan translation keys**: Choose descriptive, hierarchical names - -### Step 2: Add Translation Keys - -**Update all three language files:** - -#### English (`src/i18n/locales/en.json`) -```json -{ - "feature": { - "newFeature": "New Feature", - "description": "This is a new feature description" - } -} -``` - -#### Spanish (`src/i18n/locales/es.json`) -```json -{ - "feature": { - "newFeature": "Nueva Caracterรญstica", - "description": "Esta es una descripciรณn de nueva caracterรญstica" - } -} -``` - -#### French (`src/i18n/locales/fr.json`) -```json -{ - "feature": { - "newFeature": "Nouvelle Fonctionnalitรฉ", - "description": "Ceci est une description de nouvelle fonctionnalitรฉ" - } -} -``` - -### Step 3: Update Components - -```typescript -'use client'; - -import { useTranslations } from 'next-intl'; - -export default function NewComponent() { - const t = useTranslations(); - - return ( -
-

{t('feature.newFeature')}

-

{t('feature.description')}

- -
- ); -} -``` - -## Translation Key Naming Guidelines - -### 1. Use Descriptive Names - -```typescript -// โŒ Poor naming -t('msg1') - -// โœ… Good naming -t('chat.message.private.placeholder') -``` - -### 2. Organize Hierarchically - -```json -{ - "common": { - "actions": { - "save": "Save", - "cancel": "Cancel", - "delete": "Delete" - }, - "status": { - "loading": "Loading...", - "error": "Error", - "success": "Success" - } - }, - "navigation": { - "main": { - "home": "Home", - "chat": "Chat", - "groups": "Groups" - }, - "footer": { - "about": "About", - "contact": "Contact" - } - } -} -``` - -### 3. Be Context-Aware - -```json -{ - "chat": { - "message": { - "placeholder": "Type your message...", - "send": "Send", - "attach": "Attach File" - }, - "status": { - "online": "Online", - "offline": "Offline", - "typing": "Typing..." - } - } -} -``` - -## Common Translation Patterns - -### Form Labels - -```json -{ - "forms": { - "login": { - "email": "Email Address", - "password": "Password", - "submit": "Sign In" - }, - "registration": { - "username": "Choose Username", - "email": "Email Address", - "password": "Create Password", - "submit": "Create Account" - } - } -} -``` - -### Error Messages - -```json -{ - "errors": { - "validation": { - "required": "This field is required", - "email": "Please enter a valid email address", - "password": "Password must be at least 8 characters" - }, - "network": { - "connection": "Network connection error", - "timeout": "Request timed out", - "server": "Server error occurred" - } - } -} -``` - -### Success Messages - -```json -{ - "success": { - "registration": "Account created successfully!", - "login": "Welcome back!", - "profile": "Profile updated successfully" - } -} -``` - -## Testing Your Changes - -### 1. Development Testing - -```bash -# Start development server -npm run dev - -# Test in different languages: -# - http://localhost:3000/en -# - http://localhost:3000/es -# - http://localhost:3000/fr -``` - -### 2. Language Toggle Testing - -1. Click the language toggle in the header -2. Verify the URL changes (e.g., `/en` โ†’ `/es`) -3. Check that all text translates correctly -4. Ensure language preference persists on page reload - -### 3. Component Testing - -```typescript -import { render, screen } from '@testing-library/react'; -import { NextIntlClientProvider } from 'next-intl'; -import MyComponent from './MyComponent'; - -test('translates text correctly', () => { - const messages = require('../i18n/locales/es.json'); - - render( - - - - ); - - expect(screen.getByText('Cargando...')).toBeInTheDocument(); -}); -``` - -## Adding New Languages - -### Step-by-Step Process - -1. **Update Routing**: Edit `src/i18n/routing.ts` - ```typescript - export const routing = defineRouting({ - locales: ['en', 'es', 'fr', 'de'], // Add new language - defaultLocale: 'en' - }); - ``` - -2. **Create Translation File**: Create `src/i18n/locales/de.json` - -3. **Update Language Toggle**: Add language to `src/components/LanguageToggle.tsx` - -4. **Test**: Verify new language works correctly - -## Best Practices Checklist - -### Before Submitting Changes - -- [ ] All new text uses translation keys (no hardcoded strings) -- [ ] Translation keys exist in all three language files (en, es, fr) -- [ ] Translation keys follow naming conventions -- [ ] Text is contextually appropriate for each language -- [ ] Components render correctly in all languages -- [ ] Language toggle works properly -- [ ] URLs reflect selected language -- [ ] No console errors in browser - -### Code Review Checklist - -- [ ] Translation files are properly formatted JSON -- [ ] Translation keys are descriptive and organized -- [ ] No missing translations in any language -- [ ] Components use `useTranslations` hook correctly -- [ ] Language switching functionality works -- [ ] Tests cover translation functionality - -## Common Pitfalls to Avoid - -### 1. Missing Translations - -```typescript -// โŒ This will cause errors if key doesn't exist -t('feature.nonexistent') - -// โœ… Check if key exists first -t('feature.available') || 'Default Text' -``` - -### 2. Inconsistent Naming - -```json -// โŒ Inconsistent -{ - "userProfile": "User Profile", - "account_settings": "Account Settings", - "notificationSystem": "Notifications" -} - -// โœ… Consistent camelCase -{ - "userProfile": "User Profile", - "accountSettings": "Account Settings", - "notificationSystem": "Notifications" -} -``` - -### 3. Not Considering Text Length - -Some languages (like German) produce longer text: - -```json -{ - "button": { - "save": "Save", - "cancel": "Cancel" - } -} -``` - -German: "Speichern" (longer than "Save") - -**Solution**: Design UI components to handle text expansion gracefully. - -### 4. Ignoring Cultural Context - -```json -// โŒ English-centric -{ - "greeting": "Hello {name}!" -} - -// โœ… Culturally appropriate -{ - "greeting": { - "morning": "Good morning {name}!", - "afternoon": "Good afternoon {name}!", - "evening": "Good evening {name}!" - } -} -``` - -## Troubleshooting - -### Translation Not Showing - -1. Check key exists in all language files -2. Verify key name spelling -3. Ensure component is a client component -4. Check browser console for errors - -### Language Toggle Not Working - -1. Verify routing configuration -2. Check middleware setup -3. Ensure language files are valid JSON -4. Test with `npm run dev` - -### JSON Validation Errors - -```bash -# Validate JSON syntax -node -e "JSON.parse(require('fs').readFileSync('src/i18n/locales/en.json'))" -``` - -## Getting Help - -### Resources - -1. **Next.js i18n Documentation**: https://nextjs.org/docs/app/building-your-application/routing/internationalization -2. **next-intl Documentation**: https://next-intl-docs.vercel.app/ -3. **Project Documentation**: `INTERNATIONALIZATION.md` - -### When to Ask for Help - -- Complex translation scenarios -- RTL language support -- Performance issues -- Testing strategies -- Integration problems - -### Code Review - -For significant i18n changes, request review from: - -- Team members familiar with i18n -- Design team (for UI/UX considerations) -- Product team (for user experience) - -## Continuous Improvement - -### Monitor Translation Usage - -- Track which keys are most used -- Identify missing translations -- Monitor translation completeness - -### Update Documentation - -When adding new patterns or fixing issues: - -1. Update `INTERNATIONALIZATION.md` -2. Add examples to this guide -3. Document any new processes - -### Performance Monitoring - -- Track translation file loading times -- Monitor bundle size impact -- Check for unnecessary translations - ---- - -**Remember**: Good internationalization is an ongoing process. Continuously improve translations and user experience across all supported languages. - -*Happy translating! ๐ŸŒ* \ No newline at end of file diff --git a/next-frontend/INTERNATIONALIZATION.md b/next-frontend/INTERNATIONALIZATION.md deleted file mode 100644 index 38306b3..0000000 --- a/next-frontend/INTERNATIONALIZATION.md +++ /dev/null @@ -1,147 +0,0 @@ -# Internationalization (i18n) Implementation Guide - -This document provides comprehensive information about the internationalization (i18n) implementation in BlockBelle. - -## Overview - -BlockBelle uses **next-intl** as the primary i18n library for React and Next.js applications. The implementation supports: - -- **Languages**: English (default), Spanish, and French -- **Dynamic Language Switching**: Users can switch languages without page reload -- **URL-based Localization**: URLs include locale prefixes (`/en`, `/es`, `/fr`) -- **Server-side Rendering**: Proper server-side translation support -- **Client-side Hydration**: Smooth client-side language switching - -## File Structure - -``` -src/ -โ”œโ”€โ”€ i18n/ -โ”‚ โ”œโ”€โ”€ locales/ -โ”‚ โ”‚ โ”œโ”€โ”€ en.json # English translations -โ”‚ โ”‚ โ”œโ”€โ”€ es.json # Spanish translations -โ”‚ โ”‚ โ””โ”€โ”€ fr.json # French translations -โ”‚ โ”œโ”€โ”€ index.ts # i18n configuration -โ”‚ โ””โ”€โ”€ routing.ts # Routing configuration -โ”œโ”€โ”€ middleware.ts # Internationalized routing middleware -โ””โ”€โ”€ app/ - โ”œโ”€โ”€ [locale]/ - โ”‚ โ”œโ”€โ”€ layout.tsx # Locale-specific layout - โ”‚ โ””โ”€โ”€ page.tsx # Locale-specific page - โ””โ”€โ”€ page.tsx # Root redirect to default locale -``` - -## Translation Files - -Translation files are located in `src/i18n/locales/` and use JSON format. Each file contains translation keys organized by sections. - -## Usage in Components - -### Basic Translation Usage - -```typescript -'use client'; - -import { useTranslations } from 'next-intl'; - -export default function MyComponent() { - const t = useTranslations(); - - return ( -
-

{t('homepage.welcome')}

-

{t('common.loading')}

-
- ); -} -``` - -## Language Toggle Component - -The `LanguageToggle` component provides a dropdown interface for switching between languages: - -```typescript -import LanguageToggle from '@/components/LanguageToggle'; - -// In your component -
- -
-``` - -## Adding New Languages - -### 1. Add Language to Routing - -Edit `src/i18n/routing.ts`: - -```typescript -export const routing = defineRouting({ - locales: ['en', 'es', 'fr', 'de'], // Add new language code - defaultLocale: 'en' -}); -``` - -### 2. Create Translation File - -Create `src/i18n/locales/de.json` with translation keys. - -### 3. Update Language Toggle - -Add the new language to the `LanguageToggle` component. - -## Best Practices - -### 1. Key Naming Conventions - -- Use **camelCase** for keys: `navigation.home` -- Group related keys: `navigation.home`, `navigation.chat` -- Be descriptive: `error.networkConnection` instead of `error.1` - -### 2. Avoiding Hardcoded Text - -Always use translation keys instead of hardcoded strings: - -```typescript -// โŒ Bad -

Welcome to BlockBelle

- -// โœ… Good -

{t('homepage.welcome')}

-``` - -## URL Structure - -- **English**: `https://yourdomain.com/` (default) -- **Spanish**: `https://yourdomain.com/es/` -- **French**: `https://yourdomain.com/fr/` -- **German**: `https://yourdomain.com/de/` (when added) - -## Troubleshooting - -### Common Issues - -1. **Translation Key Not Found** - - Ensure the key exists in all translation files - - Check for typos in key names - -2. **Locale Not Working** - - Verify the locale is added to `routing.ts` - - Check middleware configuration - -3. **Component Not Re-rendering** - - Ensure `useTranslations` hook is used correctly - - Check that the component is a client component - -## Contributing - -When adding new text to the application: - -1. **Always use translation keys** - never hardcode text -2. **Update all language files** - ensure translations exist for EN, ES, and FR -3. **Follow naming conventions** - use descriptive, hierarchical keys -4. **Test in all languages** - verify translations display correctly - ---- - -*This documentation is maintained as part of the BlockBelle project.* \ No newline at end of file diff --git a/next-frontend/e2e/.env.example b/next-frontend/e2e/.env.example new file mode 100644 index 0000000..0e50f38 --- /dev/null +++ b/next-frontend/e2e/.env.example @@ -0,0 +1,88 @@ +# E2E Test Environment Configuration +# Copy this file to .env and update the values as needed + +# Test Mode Configuration +NEXT_PUBLIC_TEST_MODE=true +NEXT_PUBLIC_E2E_TESTING=true +NEXT_PUBLIC_MOCK_CONTRACTS=true + +# Mock Contract Addresses for Testing +NEXT_PUBLIC_REGISTRY_ADDRESS=0xA72B585c6b2293177dd485Ec0A607A471976771B +NEXT_PUBLIC_CHAT_ADDRESS=0x562456dBF6F21d40C96D392Ef6eD1de2e921bF2C + +# Test Database (if using separate test database) +DATABASE_URL=postgresql://test:test@localhost:5432/blockbelle_test +TEST_DATABASE_URL=postgresql://test:test@localhost:5432/blockbelle_e2e_test + +# Network Configuration +NEXT_PUBLIC_CHAIN_ID=0xa4ec +NEXT_PUBLIC_RPC_URL=https://forno.celo.org + +# Authentication Test Configuration +TEST_WALLET_ADDRESS=0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A +TEST_PRIVATE_KEY=mock_private_key_for_testing_only + +# Mock API Responses +MOCK_API_DELAY=500 +MOCK_CONTRACT_RESPONSE_DELAY=1000 + +# Test Performance Configuration +TEST_TIMEOUT=30000 +TEST_RETRIES=2 +TEST_WORKERS=3 + +# Security Test Configuration +ENABLE_SECURITY_TESTS=true +ENABLE_RATE_LIMIT_TESTS=true +ENABLE_XSS_TESTS=true + +# Mobile Testing Configuration +MOBILE_VIEWPORT_WIDTH=375 +MOBILE_VIEWPORT_HEIGHT=667 +TABLET_VIEWPORT_WIDTH=768 +TABLET_VIEWPORT_HEIGHT=1024 + +# Browser Configuration +DEFAULT_BROWSER=chromium +HEADLESS_MODE=true +VIDEO_RECORDING=true +SCREENSHOT_ON_FAILURE=true + +# CI/CD Configuration +CI=true +GITHUB_ACTIONS=true +ARTIFACT_RETENTION_DAYS=30 + +# Logging Configuration +DEBUG=pw:api +VERBOSE_LOGGING=false +CONSOLE_LOGGING=true + +# Mock Data Configuration +MOCK_USER_COUNT=10 +MOCK_MESSAGE_COUNT=50 +MOCK_GROUP_COUNT=5 + +# External Service Mocking +MOCK_ENS_RESOLVER=true +MOCK_IPFS_UPLOADS=true +MOCK_NOTIFICATION_SERVICE=true + +# Rate Limiting Test Configuration +RATE_LIMIT_TEST_MESSAGES=10 +RATE_LIMIT_TEST_INTERVAL=100 + +# Performance Test Thresholds +MAX_PAGE_LOAD_TIME=5000 +MAX_MESSAGE_SEND_TIME=3000 +MAX_CHAT_LOAD_TIME=4000 + +# Accessibility Testing +ENABLE_A11Y_TESTS=true +CONTRAST_CHECKS=true +SCREEN_READER_SIMULATION=true + +# Network Conditions Testing +SLOW_NETWORK_SIMULATION=true +OFFLINE_SIMULATION=true +INTERMITTENT_CONNECTION_SIMULATION=true \ No newline at end of file diff --git a/next-frontend/e2e/CHANGELOG.md b/next-frontend/e2e/CHANGELOG.md new file mode 100644 index 0000000..1943ab9 --- /dev/null +++ b/next-frontend/e2e/CHANGELOG.md @@ -0,0 +1,167 @@ +# E2E Test Suite Changelog + +All notable changes to the BlockBelle E2E test suite will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-12-02 + +### Added +- **Initial E2E Test Suite** - Comprehensive end-to-end testing for BlockBelle chat functionality +- **Playwright Framework** - Multi-browser testing support (Chromium, Firefox, WebKit, Mobile) +- **ENS-Verified User Tests** - Complete test coverage for verified users with full feature access +- **Non-Verified User Tests** - Tests for users with limited access and upgrade prompts +- **Message Delivery Tests** - Verification of successful and failed message scenarios +- **Group Chat Tests** - Group creation, joining, management, and messaging functionality +- **Authentication Tests** - Login/logout flows, session management, and error handling +- **Security Tests** - Route protection, input validation, rate limiting, and CSRF protection +- **Test Fixtures** - Reusable test utilities, mock data, and blockchain simulation +- **CI/CD Integration** - GitHub Actions workflow for automated testing +- **Test Documentation** - Comprehensive README with setup and usage instructions +- **Environment Configuration** - Example environment file with all necessary settings +- **Test Runner Script** - Shell script for convenient test execution + +### Test Coverage + +#### Chat Functionality +- โœ… Private messaging between users +- โœ… Message delivery confirmation +- โœ… Message history and timestamps +- โœ… User presence indicators +- โœ… Message search functionality +- โœ… Real-time message updates + +#### User Types +- โœ… ENS-verified users with full features +- โœ… Non-verified users with limited access +- โœ… Verification status display +- โœ… Upgrade prompts and restrictions + +#### Group Features +- โœ… Group creation and management +- โœ… Group joining (public and private) +- โœ… Member management and permissions +- โœ… Group messaging and mentions +- โœ… Group admin controls + +#### Authentication +- โœ… Wallet connection/disconnection +- โœ… Session persistence +- โœ… Authentication state management +- โœ… Error handling and recovery +- โœ… Multi-account handling + +#### Security +- โœ… Route access control +- โœ… Input validation and sanitization +- โœ… XSS protection +- โœ… CSRF token validation +- โœ… Rate limiting +- โœ… Message encryption verification + +#### Browser Compatibility +- โœ… Desktop Chrome (Chromium) +- โœ… Desktop Firefox +- โœ… Desktop Safari (WebKit) +- โœ… Mobile Chrome (Pixel 5) +- โœ… Mobile Safari (iPhone 12) + +### Technical Implementation + +#### Mock Infrastructure +- Blockchain contract simulation +- Wallet provider mocking +- Network request interception +- Realistic user data simulation + +#### Test Utilities +- Custom Playwright fixtures +- Blockchain network mocking +- Contract response simulation +- Realistic user scenario creation + +#### CI/CD Pipeline +- Multi-matrix testing (Node.js versions ร— browsers) +- Artifact collection and reporting +- Performance and security test isolation +- Automated test summary generation + +### Development Tools + +#### Test Runner +- Shell script with colored output +- Environment checking +- Multiple execution modes +- Dependency management + +#### Documentation +- Comprehensive README +- Setup and configuration guides +- Troubleshooting documentation +- Contributing guidelines + +### Dependencies +- Playwright 1.48.0+ +- Node.js 18+ +- TypeScript 5.9+ + +### Configuration +- Multi-browser testing setup +- Environment variable configuration +- Mobile viewport testing +- Performance threshold settings + +### Future Enhancements +- Visual regression testing +- Performance benchmarking +- Accessibility testing expansion +- Multi-language testing +- API contract testing + +--- + +## Test Execution Examples + +### Run All Tests +```bash +npm run test:e2e +``` + +### Run Specific Test Category +```bash +./run-tests.sh --category chat +./run-tests.sh --security +``` + +### Run with UI Mode +```bash +npm run test:e2e:ui +./run-tests.sh --ui +``` + +### Environment Setup +```bash +# Install dependencies +./run-tests.sh --install-deps + +# Install browsers +./run-tests.sh --install-browsers + +# Check environment +./run-tests.sh --check +``` + +### CI/CD Integration +Tests automatically run on: +- Push to main/develop branches +- Pull requests +- Multi-browser matrix +- Security-focused jobs + +### Test Reports +After test execution: +- HTML reports in `playwright-report/` +- Screenshots on failure +- Video recordings +- Console logs and traces \ No newline at end of file diff --git a/next-frontend/e2e/README.md b/next-frontend/e2e/README.md new file mode 100644 index 0000000..c629598 --- /dev/null +++ b/next-frontend/e2e/README.md @@ -0,0 +1,347 @@ +# E2E Tests for BlockBelle Chat + +This directory contains comprehensive end-to-end (E2E) tests for BlockBelle's chat functionality, built with [Playwright](https://playwright.dev/). + +## Overview + +The E2E tests verify real user interactions across different scenarios: + +- **ENS-Verified User Chat Flows** - Tests for users with completed ENS verification +- **Non-Verified User Chat Flows** - Tests for users without verification +- **Message Delivery Scenarios** - Tests for successful/failed message delivery +- **Group Chat Functionality** - Tests for creating and managing group chats +- **Login/Logout Flows** - Tests for authentication and session management +- **Security Protections** - Tests for security measures and route protection + +## Test Structure + +``` +e2e/ +โ”œโ”€โ”€ fixtures/ +โ”‚ โ””โ”€โ”€ chat-fixtures.ts # Shared test fixtures and utilities +โ”œโ”€โ”€ tests/ +โ”‚ โ”œโ”€โ”€ chat/ +โ”‚ โ”‚ โ”œโ”€โ”€ ens-verified-chat.spec.ts +โ”‚ โ”‚ โ”œโ”€โ”€ non-verified-chat.spec.ts +โ”‚ โ”‚ โ”œโ”€โ”€ message-delivery.spec.ts +โ”‚ โ”‚ โ””โ”€โ”€ group-chat.spec.ts +โ”‚ โ”œโ”€โ”€ auth/ +โ”‚ โ”‚ โ””โ”€โ”€ login-logout.spec.ts +โ”‚ โ””โ”€โ”€ security/ +โ”‚ โ””โ”€โ”€ chat-security.spec.ts +โ”œโ”€โ”€ README.md # This file +โ””โ”€โ”€ test-utils.ts # Additional test utilities +``` + +## Getting Started + +### Prerequisites + +1. **Node.js** (v18 or higher) +2. **npm** or **yarn** +3. **Playwright browsers** (will be installed automatically) + +### Installation + +1. **Install Playwright dependencies:** + ```bash + cd next-frontend + npm run e2e:install + ``` + +2. **Install project dependencies:** + ```bash + npm install + ``` + +### Running Tests + +#### Run all E2E tests: +```bash +npm run test:e2e +``` + +#### Run tests with UI mode: +```bash +npm run test:e2e:ui +``` + +#### Run tests in headed mode (see browser): +```bash +npm run test:e2e:headed +``` + +#### Run specific test file: +```bash +npx playwright test tests/chat/ens-verified-chat.spec.ts +``` + +#### Run tests for specific user type: +```bash +npx playwright test --grep "ENS-Verified" +``` + +#### Run security tests only: +```bash +npx playwright test tests/security/ +``` + +### Test Reports + +After running tests, view the HTML report: +```bash +npm run e2e:show-report +``` + +## Test Configuration + +### Playwright Configuration (`playwright.config.ts`) + +The tests are configured to run on multiple browsers: +- **Chromium** (Desktop Chrome) +- **Firefox** (Desktop Firefox) +- **WebKit** (Desktop Safari) +- **Mobile Chrome** (Pixel 5) +- **Mobile Safari** (iPhone 12) + +### Environment Variables + +Create a `.env` file in the project root: + +```env +# Test environment configuration +NEXT_PUBLIC_TEST_MODE=true +NEXT_PUBLIC_MOCK_CONTRACTS=true +NEXT_PUBLIC_E2E_TESTING=true + +# Mock contract addresses for testing +NEXT_PUBLIC_REGISTRY_ADDRESS=0xTestRegistry123 +NEXT_PUBLIC_CHAT_ADDRESS=0xTestChat123 + +# Test database (if using test database) +DATABASE_URL=postgresql://test:test@localhost:5432/blockbelle_test +``` + +## Test Fixtures + +### Mock Users + +The tests use predefined mock users: + +- **Alice Johnson** (`0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A`) - ENS verified user +- **Bob Smith** (`0x8ba1f109551bD432803012645Hac136c32c3c0c4`) - ENS verified user +- **Charlie User** (`0x1234567890123456789012345678901234567890`) - Non-verified user +- **Dave Developer** (`0x9876543210987654321098765432109876543210`) - ENS verified user + +### Custom Test Fixtures + +Available fixtures in `fixtures/chat-fixtures.ts`: + +- `mockWalletConnection(user)` - Mock wallet connection for a user +- `setupMockContract()` - Mock smart contract responses +- `waitForChatLoad()` - Wait for chat interface to load +- `sendMessage(content)` - Send a message with verification +- `selectChat(userAddress)` - Select a specific chat +- `verifyMessageDelivery(content)` - Verify message was delivered + +## Test Scenarios + +### 1. ENS-Verified User Chat Flows + +Tests for users who have completed ENS verification: +- โœ… Full feature access +- โœ… Verification badges display +- โœ… Enhanced messaging capabilities +- โœ… Priority support features + +### 2. Non-Verified User Chat Flows + +Tests for users without ENS verification: +- โš ๏ธ Limited feature access +- โš ๏ธ Upgrade prompts and restrictions +- โš ๏ธ Basic messaging only +- โš ๏ธ Rate limiting and restrictions + +### 3. Message Delivery Scenarios + +Tests for message handling: +- โœ… Successful message delivery +- โŒ Failed delivery and error handling +- ๐Ÿ”„ Network issues and retry mechanisms +- ๐Ÿ“… Message ordering and timestamps + +### 4. Group Chat Functionality + +Tests for group features: +- ๐Ÿ‘ฅ Group creation and joining +- ๐Ÿ”’ Private vs public groups +- ๐Ÿ“ Group messaging and mentions +- โš™๏ธ Group management and permissions + +### 5. Authentication Flows + +Tests for login/logout: +- ๐Ÿ” Wallet connection/disconnection +- ๐Ÿ“ฑ Session management +- ๐Ÿ”‘ Authentication state persistence +- โŒ Error handling and recovery + +### 6. Security Protections + +Tests for security measures: +- ๐Ÿ›ก๏ธ Route access control +- ๐Ÿงน Input validation and sanitization +- ๐Ÿšซ Rate limiting and abuse prevention +- ๐Ÿ”’ Message encryption and privacy +- ๐Ÿ›ก๏ธ CSRF protection + +## Mock Data and Utilities + +### Contract Mocking + +Tests mock smart contract responses for: +- `getAllUsers()` - Returns list of registered users +- `getUserDetails()` - Returns user profile information +- `getConversation()` - Returns message history +- `sendMessage()` - Simulates message sending + +### Network Simulation + +Tests simulate various network conditions: +- Successful requests +- Network timeouts +- Server errors +- Rate limiting responses + +## Continuous Integration + +### GitHub Actions + +Tests can be run in CI using the included workflow configuration: + +```yaml +name: E2E Tests +on: [push, pull_request] +jobs: + e2e-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm ci + - run: npm run e2e:install + - run: npm run test:e2e +``` + +### Environment Setup for CI + +1. **Install Playwright browsers:** + ```bash + npx playwright install --with-deps + ``` + +2. **Set environment variables:** + ```bash + export NEXT_PUBLIC_TEST_MODE=true + export CI=true + ``` + +3. **Run tests:** + ```bash + npm run test:e2e + ``` + +## Debugging Tests + +### Common Issues + +1. **Tests failing due to timeouts:** + - Increase timeout values in test configuration + - Check if mock data is being loaded correctly + +2. **Element not found errors:** + - Verify selectors match actual DOM elements + - Check if page is fully loaded before interacting + +3. **Mock data not working:** + - Ensure mock scripts are loaded before page interactions + - Verify mock contract addresses match test configuration + +### Debug Commands + +```bash +# Run tests with debug output +DEBUG=pw:api npx playwright test + +# Record test execution +npx playwright test --record-video + +# Take screenshots on failure +npx playwright test --screenshot=only-on-failure + +# Generate test artifacts +npx playwright test --tracing=on +``` + +## Contributing + +### Writing New Tests + +1. **Create test file** in appropriate directory: + ``` + e2e/tests/chat/new-feature.spec.ts + ``` + +2. **Use existing fixtures** and mock data where possible + +3. **Follow naming conventions:** + - Test files: `*.spec.ts` + - Test suites: `describe('Feature Name')` + - Test cases: `it('should do something')` + +4. **Add test data attributes** to components for reliable selection: + ```tsx + + ``` + +### Test Best Practices + +1. **Use semantic test names** that describe the scenario +2. **Mock external dependencies** (contracts, APIs) +3. **Clean up test data** between tests +4. **Test both success and failure cases** +5. **Use realistic user scenarios** +6. **Add assertions for UI state changes** + +## Troubleshooting + +### Common Problems + +1. **Playwright browsers not installed:** + ```bash + npx playwright install --with-deps + ``` + +2. **Tests timing out:** + - Increase timeout in `playwright.config.ts` + - Check network conditions in tests + +3. **Mock data not working:** + - Verify mock scripts are injected correctly + - Check console for JavaScript errors + +4. **Authentication issues:** + - Ensure wallet mocking is complete + - Check session management in tests + +### Getting Help + +- ๐Ÿ“š [Playwright Documentation](https://playwright.dev/) +- ๐Ÿ› Report issues in the project repository +- ๐Ÿ’ฌ Check existing test patterns in the codebase + +## License + +These E2E tests are part of the BlockBelle project and follow the same license terms. \ No newline at end of file diff --git a/next-frontend/e2e/fixtures/chat-fixtures.ts b/next-frontend/e2e/fixtures/chat-fixtures.ts new file mode 100644 index 0000000..1fa2ccd --- /dev/null +++ b/next-frontend/e2e/fixtures/chat-fixtures.ts @@ -0,0 +1,262 @@ +/** + * E2E Test Fixtures for BlockBelle Chat Application + * + * This file contains shared test fixtures and utilities for E2E testing. + * It provides mock data, helper functions, and setup/teardown utilities. + */ + +import { test as base, Page } from '@playwright/test' + +export interface MockUser { + address: string + ensName?: string + displayName: string + isVerified: boolean + avatar?: string + bio?: string +} + +export interface MockMessage { + sender: string + receiver: string + content: string + timestamp: number + type: 'private' | 'group' +} + +export const MOCK_USERS: MockUser[] = [ + { + address: '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + ensName: 'alice.eth', + displayName: 'Alice Johnson', + isVerified: true, + avatar: 'https://example.com/avatars/alice.jpg', + bio: 'Blockchain developer and ENS enthusiast' + }, + { + address: '0x8ba1f109551bD432803012645Hac136c32c3c0c4', + ensName: 'bob.smith', + displayName: 'Bob Smith', + isVerified: true, + avatar: 'https://example.com/avatars/bob.jpg', + bio: 'Web3 designer' + }, + { + address: '0x1234567890123456789012345678901234567890', + displayName: 'Charlie User', + isVerified: false, + bio: 'New user exploring the platform' + }, + { + address: '0x9876543210987654321098765432109876543210', + ensName: 'dave.dev', + displayName: 'Dave Developer', + isVerified: true, + avatar: 'https://example.com/avatars/dave.jpg' + } +] + +export const MOCK_MESSAGES: MockMessage[] = [ + { + sender: '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + receiver: '0x8ba1f109551bD432803012645Hac136c32c3c0c4', + content: 'Hey Bob! How is the new project going?', + timestamp: Date.now() - 3600000, + type: 'private' + }, + { + sender: '0x8ba1f109551bD432803012645Hac136c32c3c0c4', + receiver: '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + content: 'Hi Alice! It\'s going great! Just finished the smart contract.', + timestamp: Date.now() - 3500000, + type: 'private' + }, + { + sender: '0x1234567890123456789012345678901234567890', + receiver: '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + content: 'Hi Alice! I\'m new here and would love to connect.', + timestamp: Date.now() - 1800000, + type: 'private' + } +] + +export interface ChatTestFixtures { + mockWalletConnection: (user: MockUser) => Promise + setupMockContract: () => Promise + waitForChatLoad: () => Promise + sendMessage: (content: string) => Promise + selectChat: (userAddress: string) => Promise + verifyMessageDelivery: (content: string) => Promise +} + +// Custom test fixtures +export const test = base.extend({ + mockWalletConnection: async ({ page }, use) => { + await use(async (user: MockUser) => { + // Mock wallet connection + await page.addInitScript((user) => { + // Mock ethereum provider + window.ethereum = { + request: async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [user.address] + case 'eth_chainId': + return '0xa4ec' // Celo mainnet + case 'personal_sign': + return '0xmockedSignature' + default: + return null + } + }, + isMetaMask: true, + selectedAddress: user.address + } + + // Mock localStorage for user preferences + localStorage.setItem('connectedAddress', user.address) + localStorage.setItem('userProfile', JSON.stringify({ + address: user.address, + displayName: user.displayName, + isVerified: user.isVerified, + ensName: user.ensName + })) + }, user) + }) + }, + + setupMockContract: async ({ page }, use) => { + await use(async () => { + // Mock contract responses + await page.addInitScript(() => { + // Mock wagmi readContract + window.readContractMock = async (contractAddress: string, functionName: string, args?: any[]) => { + switch (functionName) { + case 'getAllUsers': + return [ + '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + '0x8ba1f109551bD432803012645Hac136c32c3c0c4', + '0x1234567890123456789012345678901234567890', + '0x9876543210987654321098765432109876543210' + ] + case 'getUserDetails': + const address = args[0].toLowerCase() + const mockUsers: Record = { + '0x742d35cc6cF6b4633f82c9b7c7c31e7c7b6c8f9a': { + ensName: 'alice.eth', + avatarHash: 'QmAlice123', + registered: true + }, + '0x8ba1f109551bd432803012645hac136c32c3c0c4': { + ensName: 'bob.smith', + avatarHash: 'QmBob456', + registered: true + }, + '0x1234567890123456789012345678901234567890': { + ensName: '', + avatarHash: '', + registered: false + }, + '0x9876543210987654321098765432109876543210': { + ensName: 'dave.dev', + avatarHash: 'QmDave789', + registered: true + } + } + return mockUsers[address] || { ensName: '', avatarHash: '', registered: false } + case 'getConversation': + // Return mock conversation data + return [ + { + sender: args[0], + receiver: args[1], + content: 'Hey there!', + timestamp: Math.floor(Date.now() / 1000) - 3600 + } + ] + default: + return null + } + } + + // Mock wagmi writeContract + window.writeContractMock = async (contractAddress: string, functionName: string, args?: any[]) => { + if (functionName === 'sendMessage') { + // Simulate successful message send + return '0xmockedTransactionHash' + } + return null + } + + // Mock useReadContract hook + window.mockUseReadContract = (config: any) => ({ + data: window.readContractMock?.(config.address, config.functionName, config.args), + isLoading: false, + error: null, + refetch: () => Promise.resolve() + }) + + // Mock useWriteContract hook + window.mockUseWriteContract = () => ({ + writeContract: window.writeContractMock, + data: '0xmockedHash', + isPending: false, + error: null + }) + }) + }) + }, + + waitForChatLoad: async ({ page }, use) => { + await use(async () => { + await page.waitForSelector('[data-testid="main-chat"]', { timeout: 10000 }) + await page.waitForFunction(() => { + const chatList = document.querySelector('[data-testid="chat-list"]') + return chatList && chatList.children.length > 0 + }, { timeout: 5000 }) + }) + }, + + sendMessage: async ({ page }, use) => { + await use(async (content: string) => { + const messageInput = page.locator('[data-testid="message-input"]') + const sendButton = page.locator('[data-testid="send-button"]') + + await messageInput.fill(content) + await sendButton.click() + + // Wait for message to appear in the chat + await page.waitForFunction((msgContent) => { + const messages = document.querySelectorAll('[data-testid="message-bubble"]') + return Array.from(messages).some(message => + message.textContent?.includes(msgContent) + ) + }, content, { timeout: 5000 }) + }) + }, + + selectChat: async ({ page }, use) => { + await use(async (userAddress: string) => { + const chatItem = page.locator(`[data-testid="chat-item-${userAddress}"]`) + await chatItem.click() + + // Wait for chat to load + await page.waitForSelector('[data-testid="chat-header"]', { timeout: 3000 }) + }) + }, + + verifyMessageDelivery: async ({ page }, use) => { + await use(async (content: string) => { + await page.waitForFunction((msgContent) => { + const messages = document.querySelectorAll('[data-testid="message-bubble"]') + return Array.from(messages).some(message => { + const bubbleText = message.textContent || '' + return bubbleText.includes(msgContent) && + message.querySelector('[data-testid="message-timestamp"]') + }) + }, content, { timeout: 8000 }) + }) + } +}) + +export { expect } from '@playwright/test' \ No newline at end of file diff --git a/next-frontend/e2e/run-tests.sh b/next-frontend/e2e/run-tests.sh new file mode 100644 index 0000000..6b378a0 --- /dev/null +++ b/next-frontend/e2e/run-tests.sh @@ -0,0 +1,247 @@ +#!/bin/bash + +# E2E Test Runner Script for BlockBelle +# This script provides convenient commands for running E2E tests + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + print_error "Node.js is not installed. Please install Node.js 18+ and try again." + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + print_error "npm is not installed. Please install npm and try again." + exit 1 +fi + +print_status "Starting BlockBelle E2E Test Suite..." + +# Function to install dependencies +install_deps() { + print_status "Installing dependencies..." + npm install + print_success "Dependencies installed" +} + +# Function to install Playwright browsers +install_browsers() { + print_status "Installing Playwright browsers..." + npx playwright install --with-deps + print_success "Playwright browsers installed" +} + +# Function to run all tests +run_all_tests() { + print_status "Running all E2E tests..." + npm run test:e2e +} + +# Function to run tests with UI +run_ui_tests() { + print_status "Running E2E tests with UI mode..." + npm run test:e2e:ui +} + +# Function to run headed tests +run_headed_tests() { + print_status "Running E2E tests in headed mode..." + npm run test:e2e:headed +} + +# Function to run specific test category +run_category_tests() { + local category=$1 + if [ -z "$category" ]; then + print_error "Please specify a test category: chat, auth, security" + exit 1 + fi + + print_status "Running $category tests..." + npx playwright test tests/$category/ +} + +# Function to run single test file +run_single_test() { + local test_file=$1 + if [ -z "$test_file" ]; then + print_error "Please specify a test file path" + exit 1 + fi + + print_status "Running test: $test_file" + npx playwright test $test_file +} + +# Function to show test report +show_report() { + print_status "Generating test report..." + npm run e2e:show-report +} + +# Function to run specific user type tests +run_user_type_tests() { + local user_type=$1 + if [ -z "$user_type" ]; then + print_error "Please specify user type: verified, unverified" + exit 1 + fi + + print_status "Running tests for $user_type users..." + npx playwright test --grep="$user_type" +} + +# Function to run security tests only +run_security_tests() { + print_status "Running security-focused tests..." + npx playwright test tests/security/ +} + +# Function to clean test artifacts +clean_artifacts() { + print_status "Cleaning test artifacts..." + rm -rf test-results/ + rm -rf playwright-report/ + rm -rf playbackwright-report/ + print_success "Test artifacts cleaned" +} + +# Function to check test environment +check_environment() { + print_status "Checking test environment..." + + # Check Node.js version + node_version=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$node_version" -lt 18 ]; then + print_warning "Node.js version $node_version detected. Recommended: Node.js 18+" + else + print_success "Node.js version: $(node -v)" + fi + + # Check if Playwright is installed + if npx playwright --version &> /dev/null; then + print_success "Playwright version: $(npx playwright --version)" + else + print_warning "Playwright not found. Run with --install-browsers to install." + fi + + # Check for test environment file + if [ -f ".env" ]; then + print_success "Environment file found" + else + print_warning "No .env file found. Copy .env.example to .env and configure." + fi +} + +# Function to show usage +show_usage() { + echo "BlockBelle E2E Test Runner" + echo "" + echo "Usage: $0 [command] [options]" + echo "" + echo "Commands:" + echo " --install-deps Install all dependencies" + echo " --install-browsers Install Playwright browsers" + echo " --all Run all E2E tests" + echo " --ui Run tests with UI mode" + echo " --headed Run tests in headed mode" + echo " --category Run tests for specific category (chat|auth|security)" + echo " --single Run single test file" + echo " --verified Run tests for verified users" + echo " --unverified Run tests for unverified users" + echo " --security Run security-focused tests" + echo " --report Show test report" + echo " --clean Clean test artifacts" + echo " --check Check test environment" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 --install-deps" + echo " $0 --install-browsers" + echo " $0 --all" + echo " $0 --category chat" + echo " $0 --verified" + echo " $0 --security" + echo " $0 --single tests/chat/ens-verified-chat.spec.ts" +} + +# Parse command line arguments +case "${1:-}" in + --install-deps) + install_deps + ;; + --install-browsers) + install_browsers + ;; + --all) + run_all_tests + ;; + --ui) + run_ui_tests + ;; + --headed) + run_headed_tests + ;; + --category) + run_category_tests "$2" + ;; + --single) + run_single_test "$2" + ;; + --verified) + run_user_type_tests "verified" + ;; + --unverified) + run_user_type_tests "unverified" + ;; + --security) + run_security_tests + ;; + --report) + show_report + ;; + --clean) + clean_artifacts + ;; + --check) + check_environment + ;; + --help|-h) + show_usage + ;; + "") + print_warning "No command specified. Run with --help for usage information." + show_usage + ;; + *) + print_error "Unknown command: $1" + show_usage + exit 1 + ;; +esac \ No newline at end of file diff --git a/next-frontend/e2e/test-utils.ts b/next-frontend/e2e/test-utils.ts new file mode 100644 index 0000000..076bf59 --- /dev/null +++ b/next-frontend/e2e/test-utils.ts @@ -0,0 +1,350 @@ +/** + * Additional Test Utilities for BlockBelle E2E Tests + * + * This file contains helper functions and utilities that can be shared + * across different test files for common operations. + */ + +import { Page, Browser, BrowserContext, expect } from '@playwright/test' + +/** + * Wait for element to be visible and return when ready + */ +export async function waitForElementVisible( + page: Page, + selector: string, + timeout: number = 10000 +) { + await page.waitForSelector(selector, { timeout, state: 'visible' }) +} + +/** + * Wait for element to be clickable + */ +export async function waitForElementClickable( + page: Page, + selector: string, + timeout: number = 10000 +) { + await page.waitForSelector(selector, { timeout, state: 'visible' }) + await page.waitForFunction((sel) => { + const element = document.querySelector(sel) + return element && !element.hasAttribute('disabled') + }, selector) +} + +/** + * Type text into input with realistic delays + */ +export async function typeText( + page: Page, + selector: string, + text: string, + delay: number = 50 +) { + const element = page.locator(selector) + await element.click() + await element.fill(text) + + // Simulate realistic typing by adding delays between keypresses + await page.keyboard.press('End') + await page.keyboard.press('Backspace') + await page.keyboard.type(text, { delay }) +} + +/** + * Mock blockchain network responses + */ +export function mockBlockchainNetwork(page: Page) { + // Mock successful transaction + page.addInitScript(() => { + window.ethereum = { + request: async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return ['0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A'] + case 'eth_chainId': + return '0xa4ec' // Celo mainnet + case 'personal_sign': + return '0xmockedSignature' + default: + return null + } + }, + isMetaMask: true, + selectedAddress: '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + on: (event: string, callback: Function) => { + // Mock wallet event listeners + if (event === 'accountsChanged') { + setTimeout(() => callback(['0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A']), 100) + } + if (event === 'chainChanged') { + setTimeout(() => callback('0xa4ec'), 100) + } + }, + removeListener: () => {} + } + }) +} + +/** + * Mock contract responses with realistic data + */ +export function mockContractResponses(page: Page) { + page.addInitScript(() => { + // Mock registry contract + window.readContractMock = async (contractAddress: string, functionName: string, args?: any[]) => { + switch (functionName) { + case 'getAllUsers': + return [ + '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', // Alice + '0x8ba1f109551bD432803012645Hac136c32c3c0c4', // Bob + '0x1234567890123456789012345678901234567890', // Charlie + '0x9876543210987654321098765432109876543210' // Dave + ] + + case 'getUserDetails': + const address = args[0].toLowerCase() + const users: Record = { + '0x742d35cc6cF6b4633f82c9b7c7c31e7c7b6c8f9a': { + ensName: 'alice.eth', + avatarHash: 'QmAlice123', + registered: true, + displayName: 'Alice Johnson', + bio: 'Blockchain developer' + }, + '0x8ba1f109551bd432803012645hac136c32c3c0c4': { + ensName: 'bob.smith', + avatarHash: 'QmBob456', + registered: true, + displayName: 'Bob Smith', + bio: 'Web3 designer' + }, + '0x1234567890123456789012345678901234567890': { + ensName: '', + avatarHash: '', + registered: false, + displayName: 'Charlie User', + bio: 'New user' + }, + '0x9876543210987654321098765432109876543210': { + ensName: 'dave.dev', + avatarHash: 'QmDave789', + registered: true, + displayName: 'Dave Developer', + bio: 'Smart contract developer' + } + } + return users[address] || { ensName: '', avatarHash: '', registered: false } + + case 'getConversation': + return [ + { + sender: args[0], + receiver: args[1], + content: 'Hey there! Welcome to the chat.', + timestamp: Math.floor(Date.now() / 1000) - 3600, + messageType: 'text' + }, + { + sender: args[1], + receiver: args[0], + content: 'Hi! Thanks for the warm welcome.', + timestamp: Math.floor(Date.now() / 1000) - 3500, + messageType: 'text' + } + ] + + default: + return null + } + } + + // Mock write contract + window.writeContractMock = async (contractAddress: string, functionName: string, args?: any[]) => { + if (functionName === 'sendMessage') { + // Simulate transaction delay + await new Promise(resolve => setTimeout(resolve, 1000)) + return '0xmockedTransactionHash123456789' + } + return null + } + + // Mock verification status + window.getVerificationStatus = (address: string) => { + const verifiedUsers = [ + '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + '0x8ba1f109551bD432803012645Hac136c32c3c0c4', + '0x9876543210987654321098765432109876543210' + ] + return verifiedUsers.includes(address.toLowerCase()) + } + }) +} + +/** + * Create realistic user scenarios + */ +export async function createRealisticUserScenario(page: Page, userType: 'verified' | 'unverified' = 'verified') { + const users = { + verified: { + address: '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + ensName: 'alice.eth', + displayName: 'Alice Johnson', + isVerified: true + }, + unverified: { + address: '0x1234567890123456789012345678901234567890', + ensName: '', + displayName: 'Charlie User', + isVerified: false + } + } + + const user = users[userType] + + // Mock wallet connection + await page.addInitScript((user) => { + window.ethereum = { + request: async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [user.address] + case 'eth_chainId': + return '0xa4ec' + default: + return null + } + }, + isMetaMask: true, + selectedAddress: user.address + } + }, user) + + // Mock localStorage with user data + await page.evaluate((user) => { + localStorage.setItem('connectedAddress', user.address) + localStorage.setItem('userProfile', JSON.stringify({ + address: user.address, + displayName: user.displayName, + isVerified: user.isVerified, + ensName: user.ensName + })) + }, user) + + return user +} + +/** + * Simulate network latency and delays + */ +export async function simulateNetworkDelay(page: Page, delayMs: number = 1000) { + await page.waitForTimeout(delayMs) +} + +/** + * Mock file upload scenario + */ +export async function mockFileUpload(page: Page, fileName: string = 'test-document.pdf') { + // Create a mock file + const fileContent = 'Mock file content for testing' + const filePath = `./test-upload-${fileName}` + + // In real tests, you would create an actual file + // For now, we'll mock the file upload dialog + await page.addInitScript(() => { + const originalOpen = window.open + window.open = function(url?: string, target?: string, features?: string) { + if (url?.includes('file://')) { + return null // Mock file dialog + } + return originalOpen.call(this, url, target, features) + } + }) +} + +/** + * Verify element has expected text content + */ +export async function verifyTextContent( + page: Page, + selector: string, + expectedText: string +) { + const element = page.locator(selector) + await expect(element).toContainText(expectedText) +} + +/** + * Verify element has expected attribute value + */ +export async function verifyAttribute( + page: Page, + selector: string, + attribute: string, + expectedValue: string +) { + const element = page.locator(selector) + await expect(element).toHaveAttribute(attribute, expectedValue) +} + +/** + * Verify element has expected class + */ +export async function verifyClass( + page: Page, + selector: string, + expectedClass: string +) { + const element = page.locator(selector) + await expect(element).toHaveClass(new RegExp(expectedClass)) +} + +/** + * Wait for API call to complete + */ +export async function waitForApiCall(page: Page, urlPattern: string, timeout: number = 10000) { + const [response] = await Promise.all([ + page.waitForResponse(response => + response.url().includes(urlPattern) && response.status() < 400, + { timeout } + ) + ]) + return response +} + +/** + * Generate random test data + */ +export function generateTestData() { + return { + randomAddress: `0x${Math.random().toString(16).substr(2, 40)}`, + randomMessage: `Test message ${Date.now()} - ${Math.random().toString(36).substr(2, 9)}`, + randomEnsName: `testuser${Math.random().toString(36).substr(2, 6)}.eth`, + randomGroupName: `Test Group ${Math.random().toString(36).substr(2, 6)}` + } +} + +/** + * Clean up test data + */ +export async function cleanupTestData(page: Page) { + // Clear localStorage and sessionStorage + await page.evaluate(() => { + localStorage.clear() + sessionStorage.clear() + }) + + // Clear any test cookies + await page.context().clearCookies() +} + +/** + * Take screenshot for debugging + */ +export async function takeDebugScreenshot(page: Page, name: string) { + await page.screenshot({ + path: `./test-screenshots/${name}-${Date.now()}.png`, + fullPage: true + }) +} \ No newline at end of file diff --git a/next-frontend/e2e/tests/auth/login-logout.spec.ts b/next-frontend/e2e/tests/auth/login-logout.spec.ts new file mode 100644 index 0000000..65d25e3 --- /dev/null +++ b/next-frontend/e2e/tests/auth/login-logout.spec.ts @@ -0,0 +1,502 @@ +/** + * E2E Tests for Login and Logout Flows + * + * These tests verify authentication functionality including: + * - Wallet connection and disconnection + * - User session management + * - Authentication state persistence + * - Login error handling + */ + +import { test, expect } from '../../fixtures/chat-fixtures' +import { MOCK_USERS } from '../../fixtures/chat-fixtures' + +test.describe('Login and Logout Flows', () => { + test.beforeEach(async ({ page }) => { + // Clear any existing session + await page.context().clearCookies() + await page.goto('/') + }) + + test.describe('Wallet Connection', () => { + test('should connect wallet successfully', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] // Alice + + // Mock wallet not connected initially + await page.addInitScript(() => { + window.ethereum = { + request: async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [] // No accounts connected initially + case 'eth_chainId': + return '0xa4ec' // Celo mainnet + default: + return null + } + }, + isMetaMask: true + } + }) + + // Click connect wallet button + await page.locator('[data-testid="connect-wallet-button"]').click() + + // Select wallet (mock MetaMask) + await page.locator('[data-testid="metamask-option"]').click() + + // Mock successful wallet connection + await mockWalletConnection(user) + + // Verify wallet connected successfully + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + await expect(page.locator('[data-testid="wallet-address"]')).toContainText(user.address.slice(0, 6)) + + // Verify user profile is displayed + await expect(page.locator('[data-testid="user-profile"]')).toBeVisible() + await expect(page.locator('[data-testid="user-display-name"]')).toContainText(user.displayName) + + // Verify chat interface is accessible + await expect(page.locator('[data-testid="main-chat"]')).toBeVisible() + }) + + test('should handle wallet connection rejection', async ({ + page + }) => { + // Mock wallet that rejects connection + await page.addInitScript(() => { + window.ethereum = { + request: async ({ method, params }: any) => { + if (method === 'eth_requestAccounts') { + throw new Error('User rejected the request') + } + return [] + }, + isMetaMask: true + } + }) + + // Attempt to connect wallet + await page.locator('[data-testid="connect-wallet-button"]').click() + await page.locator('[data-testid="metamask-option"]').click() + + // Verify error handling + await expect(page.locator('[data-testid="connection-error"]')).toBeVisible() + await expect(page.locator('[data-testid="connection-error"]')).toContainText('Connection rejected') + + // Verify retry option is available + await expect(page.locator('[data-testid="retry-connection-button"]')).toBeVisible() + }) + + test('should handle MetaMask not installed', async ({ + page + }) => { + // Mock no wallet installed + await page.addInitScript(() => { + window.ethereum = undefined + }) + + // Attempt to connect wallet + await page.locator('[data-testid="connect-wallet-button"]').click() + + // Verify wallet installation prompt + await expect(page.locator('[data-testid="wallet-not-found"]')).toBeVisible() + await expect(page.locator('[data-testid="install-metamask-button"]')).toBeVisible() + + // Verify instructions are provided + await expect(page.locator('[data-testid="installation-instructions"]')).toContainText('install MetaMask') + }) + + test('should handle wrong network selection', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] + + // Mock wallet connected to wrong network + await page.addInitScript(() => { + window.ethereum = { + request: async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [user.address] + case 'eth_chainId': + return '0x1' // Ethereum mainnet instead of Celo + default: + return null + } + }, + isMetaMask: true, + selectedAddress: user.address + } + }) + + await mockWalletConnection(user) + await page.goto('/') + + // Verify network switch prompt + await expect(page.locator('[data-testid="wrong-network-warning"]')).toBeVisible() + await expect(page.locator('[data-testid="wrong-network-warning"]')).toContainText('Please switch to Celo network') + await expect(page.locator('[data-testid="switch-network-button"]')).toBeVisible() + + // Simulate network switch + await page.locator('[data-testid="switch-network-button"]').click() + await page.waitForTimeout(2000) + + // Verify network switch success + await expect(page.locator('[data-testid="network-switch-success"]')).toBeVisible() + await expect(page.locator('[data-testid="current-network"]')).toContainText('Celo') + }) + }) + + test.describe('Session Management', () => { + test('should maintain session across page refreshes', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] + await mockWalletConnection(user) + await page.goto('/') + + // Verify session is maintained + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + await expect(page.locator('[data-testid="user-profile"]')).toBeVisible() + + // Refresh page + await page.reload() + await page.waitForTimeout(3000) + + // Verify session persists + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + await expect(page.locator('[data-testid="user-display-name"]')).toContainText(user.displayName) + + // Verify chat state is preserved + await expect(page.locator('[data-testid="main-chat"]')).toBeVisible() + await expect(page.locator('[data-testid="chat-list"]')).toBeVisible() + }) + + test('should handle expired sessions', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[1] // Bob + + await mockWalletConnection(user) + await page.goto('/') + + // Wait for session to be established + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + + // Mock session expiry by clearing localStorage + await page.evaluate(() => { + localStorage.clear() + sessionStorage.clear() + }) + + // Force page reload + await page.reload() + await page.waitForTimeout(3000) + + // Verify session reset + await expect(page.locator('[data-testid="connect-wallet-button"]')).toBeVisible() + await expect(page.locator('[data-testid="wallet-connected"]')).not.toBeVisible() + }) + + test('should remember user preferences', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] + + await mockWalletConnection(user) + await page.goto('/') + + // Set user preferences + await page.locator('[data-testid="theme-toggle"]').click() + await page.locator('[data-testid="language-selector"]').selectOption('es') + + // Verify preferences are saved + await expect(page.locator('[data-testid="theme-dark"]')).toHaveClass(/active/) + await expect(page.locator('[data-testid="language-spanish"]')).toHaveClass(/active/) + + // Refresh and verify preferences persist + await page.reload() + await page.waitForTimeout(2000) + await expect(page.locator('[data-testid="theme-dark"]')).toHaveClass(/active/) + await expect(page.locator('[data-testid="language-spanish"]')).toHaveClass(/active/) + }) + }) + + test.describe('Logout Functionality', () => { + test('should disconnect wallet successfully', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[2] // Charlie (unverified) + + await mockWalletConnection(user) + await page.goto('/') + + // Verify connected state + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + + // Open user menu + await page.locator('[data-testid="user-profile-button"]').click() + + // Click disconnect + await page.locator('[data-testid="disconnect-wallet-button"]').click() + + // Confirm disconnection + await page.locator('[data-testid="confirm-disconnect"]').click() + + // Verify disconnection + await expect(page.locator('[data-testid="connect-wallet-button"]')).toBeVisible() + await expect(page.locator('[data-testid="wallet-connected"]')).not.toBeVisible() + await expect(page.locator('[data-testid="user-profile"]')).not.toBeVisible() + + // Verify chat interface is hidden + await expect(page.locator('[data-testid="main-chat"]')).not.toBeVisible() + }) + + test('should clear user data on logout', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[1] // Bob + + await mockWalletConnection(user) + await page.goto('/') + + // Add some user data + await page.evaluate(() => { + localStorage.setItem('userMessages', JSON.stringify(['test message'])) + localStorage.setItem('chatPreferences', JSON.stringify({ theme: 'dark' })) + sessionStorage.setItem('tempData', 'temp value') + }) + + // Disconnect wallet + await page.locator('[data-testid="user-profile-button"]').click() + await page.locator('[data-testid="disconnect-wallet-button"]').click() + await page.locator('[data-testid="confirm-disconnect"]').click() + + // Verify data is cleared + const userMessages = await page.evaluate(() => localStorage.getItem('userMessages')) + const chatPreferences = await page.evaluate(() => localStorage.getItem('chatPreferences')) + const tempData = await page.evaluate(() => sessionStorage.getItem('tempData')) + + expect(userMessages).toBeNull() + expect(chatPreferences).toBeNull() + expect(tempData).toBeNull() + }) + + test('should handle logout cancellation', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[3] // Dave + + await mockWalletConnection(user) + await page.goto('/') + + // Open user menu + await page.locator('[data-testid="user-profile-button"]').click() + + // Click disconnect + await page.locator('[data-testid="disconnect-wallet-button"]').click() + + // Cancel disconnection + await page.locator('[data-testid="cancel-disconnect"]').click() + + // Verify still connected + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + await expect(page.locator('[data-testid="user-profile"]')).toBeVisible() + }) + }) + + test.describe('Authentication State Management', () => { + test('should handle multiple wallet connections', async ({ + page, + mockWalletConnection + }) => { + const user1 = MOCK_USERS[0] // Alice + const user2 = MOCK_USERS[1] // Bob + + // Connect first user + await mockWalletConnection(user1) + await page.goto('/') + + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + await expect(page.locator('[data-testid="user-display-name"]')).toContainText(user1.displayName) + + // Simulate wallet account change + await page.addInitScript(() => { + window.ethereum.request = async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [user2.address] // Switch to different account + default: + return null + } + } + + // Trigger accountsChanged event + setTimeout(() => { + window.ethereum.emit('accountsChanged', [user2.address]) + }, 100) + }) + + // Wait for account change processing + await page.waitForTimeout(2000) + + // Verify user switched + await expect(page.locator('[data-testid="user-display-name"]')).toContainText(user2.displayName) + await expect(page.locator('[data-testid="account-switch-notification"]')).toBeVisible() + }) + + test('should handle wallet lock/unlock', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[2] // Charlie + + await mockWalletConnection(user) + await page.goto('/') + + // Verify connected + await expect(page.locator('[data-testid="wallet-connected"]')).toBeVisible() + + // Simulate wallet lock + await page.addInitScript(() => { + window.ethereum.request = async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [] // No accounts when locked + default: + return null + } + } + }) + + // Wait for lock detection + await page.waitForTimeout(2000) + + // Verify wallet appears disconnected + await expect(page.locator('[data-testid="wallet-disconnected-warning"]')).toBeVisible() + await expect(page.locator('[data-testid="reconnect-prompt"]')).toBeVisible() + + // Simulate unlock + await page.addInitScript(() => { + window.ethereum.request = async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [user.address] // Account available again + default: + return null + } + } + }) + + // Auto-reconnect should trigger + await page.waitForTimeout(1000) + await expect(page.locator('[data-testid="wallet-reconnected"]')).toBeVisible() + }) + + test('should validate authentication before sensitive actions', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] // Alice + + await mockWalletConnection(user) + await page.goto('/') + + // Navigate to sensitive action (e.g., group creation) + await page.locator('[data-testid="create-group-chat-button"]').click() + + // Verify authentication validation + await expect(page.locator('[data-testid="authentication-check"]')).toBeVisible() + await expect(page.locator('[data-testid="authentication-check"]')).toContainText('Confirm your identity') + + // Mock successful authentication + await page.locator('[data-testid="confirm-authentication"]').click() + + // Verify access granted + await expect(page.locator('[data-testid="group-creation-form"]')).toBeVisible() + }) + }) + + test.describe('Error Recovery', () => { + test('should recover from authentication errors', async ({ + page + }) => { + // Start with no wallet + await page.goto('/') + + // Try to access protected feature + await page.locator('[data-testid="main-chat"]').click() + + // Verify authentication required message + await expect(page.locator('[data-testid="authentication-required"]')).toBeVisible() + await expect(page.locator('[data-testid="connect-wallet-prompt"]')).toBeVisible() + + // Connect wallet to recover + await page.locator('[data-testid="connect-wallet-button"]').click() + + // Mock successful connection + await page.addInitScript(() => { + window.ethereum = { + request: async ({ method, params }: any) => { + switch (method) { + case 'eth_accounts': + return [MOCK_USERS[0].address] + default: + return null + } + }, + isMetaMask: true + } + }) + + // Simulate connection + await page.locator('[data-testid="metamask-option"]').click() + + // Verify recovery + await expect(page.locator('[data-testid="main-chat"]')).toBeVisible() + await expect(page.locator('[data-testid="authentication-required"]')).not.toBeVisible() + }) + + test('should handle network errors gracefully', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] + + await mockWalletConnection(user) + await page.goto('/') + + // Simulate network error during authentication check + await page.addInitScript(() => { + window.readContractMock = async () => { + throw new Error('Network error') + } + }) + + // Try to access contract data + await page.locator('[data-testid="refresh-chat-data"]').click() + + // Verify error handling + await expect(page.locator('[data-testid="network-error-message"]')).toBeVisible() + await expect(page.locator('[data-testid="retry-button"]')).toBeVisible() + + // Retry and verify recovery + await page.locator('[data-testid="retry-button"]').click() + await page.waitForTimeout(2000) + + // Should show loading state during retry + await expect(page.locator('[data-testid="loading-indicator"]')).toBeVisible() + }) + }) +}) \ No newline at end of file diff --git a/next-frontend/e2e/tests/chat/ens-verified-chat.spec.ts b/next-frontend/e2e/tests/chat/ens-verified-chat.spec.ts new file mode 100644 index 0000000..dd1ab9b --- /dev/null +++ b/next-frontend/e2e/tests/chat/ens-verified-chat.spec.ts @@ -0,0 +1,213 @@ +/** + * E2E Tests for ENS-Verified User Chat Flows + * + * These tests verify the chat functionality for users who have completed + * ENS (Ethereum Name Service) verification, ensuring they can: + * - Connect to the chat interface + * - Send and receive messages + * - See verification badges + * - Access all chat features + */ + +import { test, expect } from '../../fixtures/chat-fixtures' +import { MOCK_USERS } from '../../fixtures/chat-fixtures' + +test.describe('ENS-Verified User Chat Flows', () => { + let verifiedUser: typeof MOCK_USERS[0] + + test.beforeEach(async ({ page, mockWalletConnection, setupMockContract }) => { + verifiedUser = MOCK_USERS[0] // Alice - ENS verified user + await mockWalletConnection(verifiedUser) + await setupMockContract() + await page.goto('/') + }) + + test('should display chat interface with verified user status', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Verify chat interface loads + await expect(page.locator('[data-testid="main-chat"]')).toBeVisible() + + // Verify user status is shown as verified + await expect(page.locator('[data-testid="user-status"]')).toContainText('Verified') + + // Verify ENS name is displayed + await expect(page.locator('[data-testid="user-ens-name"]')).toContainText('alice.eth') + }) + + test('should allow verified user to send messages', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage, + verifyMessageDelivery + }) => { + await waitForChatLoad() + + // Select a chat with another user + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + // Send a message + const messageContent = 'Hello Bob! This is a test message from a verified user.' + await sendMessage(messageContent) + + // Verify message delivery + await verifyMessageDelivery(messageContent) + + // Verify the message shows verification status + const messageBubble = page.locator('[data-testid="message-bubble"]').filter({ + hasText: messageContent + }) + await expect(messageBubble).toBeVisible() + await expect(messageBubble.locator('[data-testid="verification-badge"]')).toBeVisible() + }) + + test('should display verification badges for verified users in chat list', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Verify that verified users show verification badges + const verifiedChatItems = page.locator('[data-testid="chat-item"]').filter({ + has: page.locator('[data-testid="verification-badge"]') + }) + await expect(verifiedChatItems).toHaveCount(2) // Alice and Dave are verified + + // Verify non-verified users don't show verification badges + const unverifiedChatItems = page.locator('[data-testid="chat-item"]').filter({ + hasNot: page.locator('[data-testid="verification-badge"]') + }) + await expect(unverifiedChatItems).toHaveCount(1) // Charlie is not verified + }) + + test('should show verification status in chat headers', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + // Select a verified user chat + const verifiedTargetUser = MOCK_USERS[3] // Dave + await selectChat(verifiedTargetUser.address) + + // Verify verification badge is shown in chat header + await expect(page.locator('[data-testid="chat-header"] [data-testid="verification-badge"]')).toBeVisible() + + // Verify ENS name is displayed + await expect(page.locator('[data-testid="chat-header-user-name"]')).toContainText('dave.dev') + }) + + test('should enable enhanced features for verified users', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[1].address) // Bob + + // Verify enhanced features are available (e.g., file sharing, voice/video calls) + await expect(page.locator('[data-testid="file-attachment-button"]')).toBeVisible() + await expect(page.locator('[data-testid="voice-call-button"]')).toBeVisible() + await expect(page.locator('[data-testid="video-call-button"]')).toBeVisible() + }) + + test('should properly display message history with verification context', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[1].address) // Bob + + // Verify existing messages show verification context + const messageBubbles = page.locator('[data-testid="message-bubble"]') + const messageCount = await messageBubbles.count() + + for (let i = 0; i < messageCount; i++) { + const messageBubble = messageBubbles.nth(i) + const senderAddress = await messageBubble.getAttribute('data-sender') + const sender = MOCK_USERS.find(user => user.address.toLowerCase() === senderAddress?.toLowerCase()) + + if (sender?.isVerified) { + await expect(messageBubble.locator('[data-testid="verification-badge"]')).toBeVisible() + } else { + await expect(messageBubble.locator('[data-testid="verification-badge"]')).not.toBeVisible() + } + } + }) + + test('should handle search functionality for verified users', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Open search functionality + await page.locator('[data-testid="search-input"]').click() + + // Search for verified users + await page.locator('[data-testid="search-input"]').fill('alice') + + // Verify search results show verification status + const searchResults = page.locator('[data-testid="search-results"] [data-testid="user-item"]') + await expect(searchResults).toHaveCount(1) + await expect(searchResults.locator('[data-testid="verification-badge"]')).toBeVisible() + }) + + test('should display correct online status for verified users', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Check online status indicators for verified users + const verifiedUserChatItems = page.locator('[data-testid="chat-item"]').filter({ + has: page.locator('[data-testid="verification-badge"]') + }) + + const count = await verifiedUserChatItems.count() + for (let i = 0; i < count; i++) { + const chatItem = verifiedUserChatItems.nth(i) + await expect(chatItem.locator('[data-testid="online-status"]')).toBeVisible() + } + }) + + test('should maintain verification status across chat sessions', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] + await selectChat(targetUser.address) + + // Send multiple messages + await sendMessage('First message from verified user') + await sendMessage('Second message from verified user') + await sendMessage('Third message from verified user') + + // Refresh the page and verify verification status persists + await page.reload() + await waitForChatLoad() + await selectChat(targetUser.address) + + // Verify all messages still show verification status + const messageBubbles = page.locator('[data-testid="message-bubble"]') + const messageCount = await messageBubbles.count() + + for (let i = 0; i < messageCount; i++) { + const messageBubble = messageBubbles.nth(i) + await expect(messageBubble.locator('[data-testid="verification-badge"]')).toBeVisible() + } + }) +}) \ No newline at end of file diff --git a/next-frontend/e2e/tests/chat/group-chat.spec.ts b/next-frontend/e2e/tests/chat/group-chat.spec.ts new file mode 100644 index 0000000..0c20de8 --- /dev/null +++ b/next-frontend/e2e/tests/chat/group-chat.spec.ts @@ -0,0 +1,477 @@ +/** + * E2E Tests for Group Chat Functionality + * + * These tests verify group chat features including: + * - Creating group chats + * - Joining existing groups + * - Managing group members + * - Sending messages in group context + * - Group permissions and access control + */ + +import { test, expect } from '../../fixtures/chat-fixtures' +import { MOCK_USERS } from '../../fixtures/chat-fixtures' + +test.describe('Group Chat Functionality', () => { + test.beforeEach(async ({ page, mockWalletConnection, setupMockContract }) => { + const user = MOCK_USERS[0] // Alice - verified user + await mockWalletConnection(user) + await setupMockContract() + await page.goto('/') + }) + + test.describe('Group Creation and Joining', () => { + test('should allow verified user to create a new group', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Click "Create Group Chat" button + await page.locator('[data-testid="create-group-chat-button"]').click() + + // Fill group creation form + await page.locator('[data-testid="group-name-input"]').fill('Blockchain Developers') + await page.locator('[data-testid="group-description-input"]').fill('A group for blockchain developers and enthusiasts') + + // Add initial members + await page.locator('[data-testid="add-member-input"]').fill(MOCK_USERS[1].address) + await page.locator('[data-testid="add-member-button"]').click() + + await page.locator('[data-testid="add-member-input"]').fill(MOCK_USERS[3].address) + await page.locator('[data-testid="add-member-button"]').click() + + // Create the group + await page.locator('[data-testid="create-group-submit"]').click() + + // Verify group creation success + await expect(page.locator('[data-testid="group-created-success"]')).toBeVisible() + await expect(page.locator('[data-testid="group-created-success"]')).toContainText('Group created successfully') + + // Verify we're now in the new group chat + await expect(page.locator('[data-testid="chat-header"] [data-testid="group-name"]')).toContainText('Blockchain Developers') + await expect(page.locator('[data-testid="member-count"]')).toContainText('3 members') + }) + + test('should allow user to join existing group', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Mock existing groups + await page.addInitScript(() => { + window.readContractMock = (contractAddress: string, functionName: string, args?: any[]) => { + switch (functionName) { + case 'getAllUsers': + return [ + '0x742d35cc6cF6B4633F82c9B7C7C31E7c7B6C8F9A', + '0x8ba1f109551bD432803012645Hac136c32c3c0c4', + '0x1234567890123456789012345678901234567890' + ] + case 'getUserGroups': + return [ + { + id: '0xgroup123', + name: 'DeFi Traders', + description: 'Decentralized Finance trading group', + memberCount: 15, + isPublic: true, + joined: true + } + ] + default: + return [] + } + } + }) + + // Navigate to group discovery + await page.locator('[data-testid="groups-tab"]').click() + + // Verify existing groups are displayed + const groupCard = page.locator('[data-testid="group-card"]').first() + await expect(groupCard.locator('[data-testid="group-name"]')).toContainText('DeFi Traders') + await expect(groupCard.locator('[data-testid="member-count"]')).toContainText('15 members') + + // Join the group + await groupCard.locator('[data-testid="join-group-button"]').click() + + // Verify join confirmation + await expect(page.locator('[data-testid="join-success-message"]')).toBeVisible() + + // Verify group appears in chat list + await page.locator('[data-testid="chat-tab"]').click() + const groupChatItem = page.locator('[data-testid="chat-item-0xgroup123"]') + await expect(groupChatItem).toBeVisible() + await expect(groupChatItem.locator('[data-testid="last-message"]')).toContainText('joined the group') + }) + + test('should handle private group joining with invitation', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Mock private group + await page.addInitScript(() => { + window.readContractMock = (contractAddress: string, functionName: string, args?: any[]) => { + if (functionName === 'getUserGroups') { + return [ + { + id: '0xprivategroup', + name: 'VIP Traders', + description: 'Private group for verified traders', + memberCount: 8, + isPublic: false, + joined: false, + hasInvitation: true + } + ] + } + return [] + } + }) + + await page.locator('[data-testid="groups-tab"]').click() + + // Verify private group is shown with invitation badge + const privateGroupCard = page.locator('[data-testid="group-card"]').first() + await expect(privateGroupCard.locator('[data-testid="invitation-badge"]')).toBeVisible() + await expect(privateGroupCard.locator('[data-testid="join-group-button"]')).toContainText('Accept Invitation') + + // Accept invitation + await privateGroupCard.locator('[data-testid="join-group-button"]').click() + + // Verify invitation acceptance + await expect(page.locator('[data-testid="invitation-accepted-message"]')).toBeVisible() + }) + + test('should validate group creation permissions for unverified users', async ({ + page, + mockWalletConnection, + waitForChatLoad + }) => { + // Switch to unverified user + const unverifiedUser = MOCK_USERS[2] // Charlie + await mockWalletConnection(unverifiedUser) + await page.reload() + + await waitForChatLoad() + + // Try to create group + await page.locator('[data-testid="create-group-chat-button"]').click() + + // Verify restriction message + await expect(page.locator('[data-testid="group-creation-restriction"]')).toBeVisible() + await expect(page.locator('[data-testid="group-creation-restriction"]')).toContainText('ENS verification required to create groups') + + // Verify upgrade prompt + await expect(page.locator('[data-testid="upgrade-to-create-groups"]')).toBeVisible() + await expect(page.locator('[data-testid="verify-ens-button"]')).toBeVisible() + }) + }) + + test.describe('Group Management', () => { + test('should allow group admin to manage members', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Create or join a group first + await page.locator('[data-testid="create-group-chat-button"]').click() + await page.locator('[data-testid="group-name-input"]').fill('Test Group') + await page.locator('[data-testid="create-group-submit"]').click() + + // Open group settings + await page.locator('[data-testid="group-settings-button"]').click() + + // Add new member + await page.locator('[data-testid="add-member-to-group"]').fill(MOCK_USERS[2].address) + await page.locator('[data-testid="add-member-submit"]').click() + + // Verify member added + await expect(page.locator('[data-testid="member-added-success"]')).toBeVisible() + + // Remove member + await page.locator('[data-testid="remove-member-button"]').first().click() + await page.locator('[data-testid="confirm-remove-member"]').click() + + // Verify member removed + await expect(page.locator('[data-testid="member-removed-success"]')).toBeVisible() + }) + + test('should display member list with verification status', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Create group with verified and unverified members + await page.locator('[data-testid="create-group-chat-button"]').click() + await page.locator('[data-testid="group-name-input"]').fill('Mixed Verification Group') + + // Add verified member + await page.locator('[data-testid="add-member-input"]').fill(MOCK_USERS[1].address) + await page.locator('[data-testid="add-member-button"]').click() + + // Add unverified member + await page.locator('[data-testid="add-member-input"]').fill(MOCK_USERS[2].address) + await page.locator('[data-testid="add-member-button"]').click() + + await page.locator('[data-testid="create-group-submit"]').click() + + // Open member list + await page.locator('[data-testid="view-members-button"]').click() + + // Verify verified members show badges + const verifiedMembers = page.locator('[data-testid="group-member-item"]').filter({ + has: page.locator('[data-testid="verification-badge"]') + }) + await expect(verifiedMembers).toHaveCount(1) // Bob is verified + + // Verify unverified members don't show badges + const unverifiedMembers = page.locator('[data-testid="group-member-item"]').filter({ + hasNot: page.locator('[data-testid="verification-badge"]') + }) + await expect(unverifiedMembers).toHaveCount(2) // Alice (creator) + Charlie + }) + + test('should handle group invitations and notifications', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Mock incoming group invitation + await page.addInitScript(() => { + window.mockNewNotification = { + type: 'group_invitation', + groupId: '0xinvitedgroup', + groupName: 'Exclusive DeFi Group', + inviterAddress: MOCK_USERS[1].address, + inviterName: 'Bob Smith' + } + }) + + // Verify invitation notification appears + await expect(page.locator('[data-testid="notification-badge"]')).toBeVisible() + await expect(page.locator('[data-testid="notification-badge"]')).toContainText('1') + + // Click notification + await page.locator('[data-testid="notifications-button"]').click() + + // Verify invitation details + const invitationCard = page.locator('[data-testid="notification-card"]').first() + await expect(invitationCard.locator('[data-testid="notification-title"]')).toContainText('Group Invitation') + await expect(invitationCard.locator('[data-testid="notification-content"]')).toContainText('Exclusive DeFi Group') + await expect(invitationCard.locator('[data-testid="notification-from"]')).toContainText('Bob Smith') + + // Accept invitation + await invitationCard.locator('[data-testid="accept-invitation-button"]').click() + + // Verify success + await expect(page.locator('[data-testid="invitation-accepted-toast"]')).toBeVisible() + }) + }) + + test.describe('Group Messaging', () => { + test('should send messages in group context', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Create group + await page.locator('[data-testid="create-group-chat-button"]').click() + await page.locator('[data-testid="group-name-input"]').fill('Test Group') + await page.locator('[data-testid="add-member-input"]').fill(MOCK_USERS[1].address) + await page.locator('[data-testid="add-member-button"]').click() + await page.locator('[data-testid="create-group-submit"]').click() + + // Send group message + const groupMessage = 'Hello everyone! This is a group message.' + await page.locator('[data-testid="message-input"]').fill(groupMessage) + await page.locator('[data-testid="send-button"]').click() + + // Verify group message delivery + await page.waitForFunction((msgContent) => { + const messages = document.querySelectorAll('[data-testid="message-bubble"]') + return Array.from(messages).some(message => + message.textContent?.includes(msgContent) + ) + }, groupMessage) + + // Verify group context is maintained + const messageBubble = page.locator('[data-testid="message-bubble"]').filter({ + hasText: groupMessage + }) + await expect(messageBubble.locator('[data-testid="group-context-indicator"]')).toBeVisible() + await expect(messageBubble.locator('[data-testid="message-type"]')).toContainText('Group') + }) + + test('should mention specific group members', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Create group + await page.locator('[data-testid="create-group-chat-button"]').click() + await page.locator('[data-testid="group-name-input"]').fill('Dev Team') + await page.locator('[data-testid="add-member-input"]').fill(MOCK_USERS[1].address) + await page.locator('[data-testid="add-member-button"]').click() + await page.locator('[data-testid="create-group-submit"]').click() + + // Send message with mention + const mentionMessage = 'Hey @bob.smith, can you review this code?' + await page.locator('[data-testid="message-input"]').fill(mentionMessage) + + // Verify mention suggestion appears + await page.locator('[data-testid="mention-suggestion"]').waitFor({ timeout: 2000 }) + await expect(page.locator('[data-testid="mention-suggestion"]')).toContainText('bob.smith') + + // Select mention + await page.locator('[data-testid="mention-suggestion"]').click() + + // Send message + await page.locator('[data-testid="send-button"]').click() + + // Verify mention is highlighted in sent message + const messageBubble = page.locator('[data-testid="message-bubble"]').filter({ + hasText: mentionMessage + }) + await expect(messageBubble.locator('[data-testid="user-mention"]')).toBeVisible() + await expect(messageBubble.locator('[data-testid="user-mention"]')).toContainText('@bob.smith') + }) + + test('should handle group message history', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Create group with mock history + await page.addInitScript(() => { + window.readContractMock = (contractAddress: string, functionName: string, args?: any[]) => { + if (functionName === 'getGroupMessages') { + return [ + { + sender: MOCK_USERS[1].address, + content: 'Welcome to the group!', + timestamp: Math.floor(Date.now() / 1000) - 3600, + messageType: 'system' + }, + { + sender: MOCK_USERS[3].address, + content: 'Thanks for having me!', + timestamp: Math.floor(Date.now() / 1000) - 3500, + messageType: 'text' + } + ] + } + return [] + } + }) + + await page.locator('[data-testid="create-group-chat-button"]').click() + await page.locator('[data-testid="group-name-input"]').fill('Existing Group') + await page.locator('[data-testid="create-group-submit"]').click() + + // Verify historical messages are loaded + await expect(page.locator('[data-testid="message-bubble"]')).toHaveCount(2) + + // Verify system message styling + const systemMessage = page.locator('[data-testid="message-bubble"]').first() + await expect(systemMessage).toHaveAttribute('data-message-type', 'system') + await expect(systemMessage.locator('[data-testid="system-message-indicator"]')).toBeVisible() + }) + }) + + test.describe('Group Security and Permissions', () => { + test('should enforce group access permissions', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Mock private group + await page.addInitScript(() => { + window.readContractMock = (contractAddress: string, functionName: string, args?: any[]) => { + if (functionName === 'getUserGroups') { + return [ + { + id: '0xprivategroup', + name: 'Private Group', + isPublic: false, + hasAccess: false, + accessReason: 'invitation_required' + } + ] + } + return [] + } + }) + + await page.locator('[data-testid="groups-tab"]').click() + + // Try to access private group without invitation + const privateGroupCard = page.locator('[data-testid="group-card"]').first() + await privateGroupCard.locator('[data-testid="group-name"]').click() + + // Verify access denied + await expect(page.locator('[data-testid="access-denied-message"]')).toBeVisible() + await expect(page.locator('[data-testid="access-denied-message"]')).toContainText('Invitation required') + await expect(page.locator('[data-testid="request-invitation-button"]')).toBeVisible() + }) + + test('should handle group admin permissions', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Create group as admin + await page.locator('[data-testid="create-group-chat-button"]').click() + await page.locator('[data-testid="group-name-input"]').fill('Admin Test Group') + await page.locator('[data-testid="create-group-submit"]').click() + + // Verify admin controls are available + await expect(page.locator('[data-testid="admin-controls"]')).toBeVisible() + await expect(page.locator('[data-testid="transfer-admin-button"]')).toBeVisible() + await expect(page.locator('[data-testid="delete-group-button"]')).toBeVisible() + + // Test member management + await page.locator('[data-testid="group-settings-button"]').click() + await page.locator('[data-testid="make-admin-button"]').click() + + // Verify admin promotion confirmation + await expect(page.locator('[data-testid="admin-promotion-success"]')).toBeVisible() + }) + + test('should prevent unauthorized group modifications', async ({ + page, + mockWalletConnection, + waitForChatLoad + }) => { + // Switch to regular member (not admin) + const regularMember = MOCK_USERS[2] // Charlie + await mockWalletConnection(regularMember) + await page.reload() + + await waitForChatLoad() + + // Try to access group settings + await page.locator('[data-testid="chat-item"]').first().click() + await page.locator('[data-testid="group-settings-button"]').click() + + // Verify admin-only controls are disabled + await expect(page.locator('[data-testid="delete-group-button"]')).toBeDisabled() + await expect(page.locator('[data-testid="transfer-admin-button"]')).toBeDisabled() + + // Verify access denied message + await expect(page.locator('[data-testid="admin-access-required"]')).toBeVisible() + }) + }) +}) \ No newline at end of file diff --git a/next-frontend/e2e/tests/chat/message-delivery.spec.ts b/next-frontend/e2e/tests/chat/message-delivery.spec.ts new file mode 100644 index 0000000..bd0dd70 --- /dev/null +++ b/next-frontend/e2e/tests/chat/message-delivery.spec.ts @@ -0,0 +1,373 @@ +/** + * E2E Tests for Message Delivery Scenarios + * + * These tests verify various message delivery scenarios including: + * - Successful message delivery + * - Failed message delivery and error handling + * - Network issues and retry mechanisms + * - Message ordering and timestamp verification + */ + +import { test, expect } from '../../fixtures/chat-fixtures' +import { MOCK_USERS, MOCK_MESSAGES } from '../../fixtures/chat-fixtures' + +test.describe('Message Delivery Scenarios', () => { + test.beforeEach(async ({ page, mockWalletConnection, setupMockContract }) => { + const user = MOCK_USERS[0] // Alice - verified user + await mockWalletConnection(user) + await setupMockContract() + await page.goto('/') + }) + + test.describe('Successful Message Delivery', () => { + test('should deliver messages successfully between verified users', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage, + verifyMessageDelivery + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + // Send various types of messages + const messages = [ + 'Hello Bob! How are you?', + 'This is a longer message to test message delivery with more content. It should wrap properly and maintain formatting.', + 'Message with special characters: @#$%^&*()_+{}|:<>?[]\\;\'",./', + 'Unicode message: ไฝ ๅฅฝไธ–็•Œ ๐ŸŒ ๐Ÿš€' + ] + + for (const message of messages) { + await sendMessage(message) + await verifyMessageDelivery(message) + } + + // Verify all messages appear in correct order + const messageBubbles = page.locator('[data-testid="message-bubble"]') + const messageCount = await messageBubbles.count() + expect(messageCount).toBe(messages.length) + + // Verify timestamps are displayed + for (let i = 0; i < messageCount; i++) { + const messageBubble = messageBubbles.nth(i) + await expect(messageBubble.locator('[data-testid="message-timestamp"]')).toBeVisible() + } + }) + + test('should deliver messages with proper sender identification', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[2] // Charlie (unverified) + await selectChat(targetUser.address) + + const messageContent = 'Message with sender identification test' + await sendMessage(messageContent) + + // Verify message shows correct sender + const messageBubble = page.locator('[data-testid="message-bubble"]').filter({ + hasText: messageContent + }) + + await expect(messageBubble).toHaveAttribute('data-sender', MOCK_USERS[0].address.toLowerCase()) + await expect(messageBubble).toHaveAttribute('data-is-own-message', 'true') + }) + + test('should update chat list with last message info', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + const messageContent = 'This should appear in chat list as last message' + await sendMessage(messageContent) + + // Go back to chat list + await page.locator('[data-testid="back-to-chat-list"]').click() + + // Verify chat item shows last message and timestamp + const chatItem = page.locator(`[data-testid="chat-item-${targetUser.address}"]`) + await expect(chatItem.locator('[data-testid="last-message"]')).toContainText(messageContent) + await expect(chatItem.locator('[data-testid="last-message-time"]')).toBeVisible() + + // Verify message is marked as read + await expect(chatItem.locator('[data-testid="unread-badge"]')).not.toBeVisible() + }) + + test('should handle rapid message sending', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[3] // Dave + await selectChat(targetUser.address) + + // Send multiple messages rapidly + const messages = Array.from({ length: 5 }, (_, i) => `Rapid message ${i + 1}`) + + for (const message of messages) { + await page.locator('[data-testid="message-input"]').fill(message) + await page.locator('[data-testid="send-button"]').click() + } + + // Wait for all messages to appear + await page.waitForFunction((expectedMessages) => { + const messageBubbles = document.querySelectorAll('[data-testid="message-bubble"]') + return messageBubbles.length >= expectedMessages + }, messages.length, { timeout: 10000 }) + + // Verify all messages are delivered in order + const messageBubbles = page.locator('[data-testid="message-bubble"]') + const messageCount = await messageBubbles.count() + expect(messageCount).toBeGreaterThanOrEqual(messages.length) + }) + }) + + test.describe('Failed Message Delivery', () => { + test('should handle network failures gracefully', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + // Simulate network failure + await page.addInitScript(() => { + window.writeContractMock = async () => { + throw new Error('Network error: Failed to send transaction') + } + }) + + // Attempt to send message + const messageContent = 'This message should fail to send' + await page.locator('[data-testid="message-input"]').fill(messageContent) + await page.locator('[data-testid="send-button"]').click() + + // Verify error handling + await expect(page.locator('[data-testid="error-message"]')).toBeVisible() + await expect(page.locator('[data-testid="error-message"]')).toContainText('Failed to send message') + + // Verify retry option is available + await expect(page.locator('[data-testid="retry-send-button"]')).toBeVisible() + + // Verify message is not displayed in chat + const messageBubble = page.locator('[data-testid="message-bubble"]').filter({ + hasText: messageContent + }) + await expect(messageBubble).not.toBeVisible() + }) + + test('should handle contract transaction failures', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[2] // Charlie + await selectChat(targetUser.address) + + // Simulate contract error + await page.addInitScript(() => { + window.writeContractMock = async () => { + throw new Error('Contract error: Insufficient gas') + } + }) + + // Attempt to send message + await page.locator('[data-testid="message-input"]').fill('Contract error test message') + await page.locator('[data-testid="send-button"]').click() + + // Verify specific error message + await expect(page.locator('[data-testid="error-message"]')).toContainText('Insufficient gas') + + // Verify suggestions are provided + await expect(page.locator('[data-testid="error-suggestions"]')).toBeVisible() + await expect(page.locator('[data-testid="error-suggestions"]')).toContainText('Try increasing gas limit') + }) + + test('should handle invalid recipient addresses', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + // Simulate invalid address response + await page.addInitScript(() => { + window.writeContractMock = async (contractAddress: string, functionName: string, args: any[]) => { + if (functionName === 'sendMessage') { + throw new Error('Invalid recipient address') + } + return null + } + }) + + await page.locator('[data-testid="message-input"]').fill('Invalid address test') + await page.locator('[data-testid="send-button"]').click() + + // Verify invalid address error + await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid recipient') + + // Verify user is prompted to select a different recipient + await expect(page.locator('[data-testid="select-different-user-prompt"]')).toBeVisible() + }) + + test('should handle message content validation errors', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[3] // Dave + await selectChat(targetUser.address) + + // Test empty message + await page.locator('[data-testid="message-input"]').fill('') + await page.locator('[data-testid="send-button"]').click() + + // Send button should be disabled for empty messages + await expect(page.locator('[data-testid="send-button"]')).toBeDisabled() + + // Test message that's too long + const longMessage = 'x'.repeat(10000) // Very long message + await page.locator('[data-testid="message-input"]').fill(longMessage) + + // Character count should show limit exceeded + await expect(page.locator('[data-testid="character-count"]')).toContainText('10000/1000') + await expect(page.locator('[data-testid="character-count"]')).toHaveClass(/text-red/) + + // Send button should be disabled + await expect(page.locator('[data-testid="send-button"]')).toBeDisabled() + }) + + test('should retry failed messages automatically', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + let attemptCount = 0 + await page.addInitScript(() => { + window.writeContractMock = async () => { + attemptCount++ + if (attemptCount < 3) { + throw new Error('Temporary network error') + } + return '0xsuccessAfterRetry' + } + }) + + const messageContent = 'This message should eventually succeed after retries' + await page.locator('[data-testid="message-input"]').fill(messageContent) + await page.locator('[data-testid="send-button"]').click() + + // Should show retry attempts + await expect(page.locator('[data-testid="retry-attempt-indicator"]')).toContainText('Retrying...') + + // Should eventually succeed + await page.waitForFunction((msgContent) => { + const messages = document.querySelectorAll('[data-testid="message-bubble"]') + return Array.from(messages).some(message => + message.textContent?.includes(msgContent) + ) + }, messageContent, { timeout: 15000 }) + + // Verify final success message + await expect(page.locator('[data-testid="success-message"]')).toContainText('Message sent successfully') + }) + }) + + test.describe('Message Ordering and Timestamps', () => { + test('should maintain message order in conversation', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[1] // Bob + await selectChat(targetUser.address) + + const messages = ['First message', 'Second message', 'Third message'] + + for (const message of messages) { + await sendMessage(message) + } + + // Verify messages appear in correct order + const messageBubbles = page.locator('[data-testid="message-bubble"]') + const messageCount = await messageBubbles.count() + + for (let i = 0; i < messages.length; i++) { + const messageBubble = messageBubbles.nth(i) + await expect(messageBubble).toContainText(messages[i]) + } + + // Verify timestamps are in ascending order + const timestamps = [] + for (let i = 0; i < messageCount; i++) { + const timestampText = await page.locator('[data-testid="message-bubble"]').nth(i) + .locator('[data-testid="message-timestamp"]').textContent() + timestamps.push(timestampText) + } + + // Timestamps should be in order (allowing for slight time differences) + expect(timestamps[0]).toBeTruthy() + expect(timestamps[1]).toBeTruthy() + expect(timestamps[2]).toBeTruthy() + }) + + test('should display message timestamps correctly', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage + }) => { + await waitForChatLoad() + + const targetUser = MOCK_USERS[2] // Charlie + await selectChat(targetUser.address) + + const messageContent = 'Timestamp test message' + await sendMessage(messageContent) + + // Verify timestamp is displayed in readable format + const timestampElement = page.locator('[data-testid="message-bubble"]') + .filter({ hasText: messageContent }) + .locator('[data-testid="message-timestamp"]') + + await expect(timestampElement).toBeVisible() + + // Timestamp should show relative time (e.g., "now", "1m ago") + const timestampText = await timestampElement.textContent() + expect(timestampText).toMatch(/(now|\d+\s*(m|h|d)\s*ago)/) + }) + }) +}) \ No newline at end of file diff --git a/next-frontend/e2e/tests/chat/non-verified-chat.spec.ts b/next-frontend/e2e/tests/chat/non-verified-chat.spec.ts new file mode 100644 index 0000000..6490b86 --- /dev/null +++ b/next-frontend/e2e/tests/chat/non-verified-chat.spec.ts @@ -0,0 +1,244 @@ +/** + * E2E Tests for Non-Verified User Chat Flows + * + * These tests verify the chat functionality for users who have not completed + * ENS verification, ensuring they can: + * - Connect to the chat interface with limited features + * - Send and receive messages with restricted functionality + * - See that they cannot access premium features + * - Experience appropriate UI limitations + */ + +import { test, expect } from '../../fixtures/chat-fixtures' +import { MOCK_USERS } from '../../fixtures/chat-fixtures' + +test.describe('Non-Verified User Chat Flows', () => { + let unverifiedUser: typeof MOCK_USERS[2] + + test.beforeEach(async ({ page, mockWalletConnection, setupMockContract }) => { + unverifiedUser = MOCK_USERS[2] // Charlie - Non-verified user + await mockWalletConnection(unverifiedUser) + await setupMockContract() + await page.goto('/') + }) + + test('should display chat interface with unverified user status', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Verify chat interface loads + await expect(page.locator('[data-testid="main-chat"]')).toBeVisible() + + // Verify user status is shown as unverified + await expect(page.locator('[data-testid="user-status"]')).toContainText('Unverified') + + // Verify no ENS name is displayed + await expect(page.locator('[data-testid="user-ens-name"]')).not.toBeVisible() + + // Verify display name shows truncated address + await expect(page.locator('[data-testid="user-display-name"]')).toContainText('123456...7890') + }) + + test('should allow unverified user to send basic messages', async ({ + page, + waitForChatLoad, + selectChat, + sendMessage, + verifyMessageDelivery + }) => { + await waitForChatLoad() + + // Select a chat with a verified user + const targetUser = MOCK_USERS[0] // Alice (verified) + await selectChat(targetUser.address) + + // Send a message + const messageContent = 'Hello Alice! I am a new user exploring the platform.' + await sendMessage(messageContent) + + // Verify message delivery + await verifyMessageDelivery(messageContent) + + // Verify the message does not show verification badge + const messageBubble = page.locator('[data-testid="message-bubble"]').filter({ + hasText: messageContent + }) + await expect(messageBubble).toBeVisible() + await expect(messageBubble.locator('[data-testid="verification-badge"]')).not.toBeVisible() + }) + + test('should limit features for unverified users in chat list', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Verify that unverified users don't show verification badges + const unverifiedChatItems = page.locator('[data-testid="chat-item"]').filter({ + hasNot: page.locator('[data-testid="verification-badge"]') + }) + await expect(unverifiedChatItems).toHaveCount(3) // Charlie + 2 others + + // Verify that verified users still show verification badges + const verifiedChatItems = page.locator('[data-testid="chat-item"]').filter({ + has: page.locator('[data-testid="verification-badge"]') + }) + await expect(verifiedChatItems).toHaveCount(2) // Alice and Dave are verified + }) + + test('should show upgrade prompts for unverified users', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[0].address) // Alice (verified user) + + // Verify upgrade prompts are visible + await expect(page.locator('[data-testid="upgrade-prompt"]')).toBeVisible() + await expect(page.locator('[data-testid="upgrade-prompt"]')).toContainText('Verify your ENS to unlock premium features') + }) + + test('should restrict enhanced features for unverified users', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[0].address) // Alice + + // Verify enhanced features are disabled or restricted + await expect(page.locator('[data-testid="file-attachment-button"]')).toBeDisabled() + await expect(page.locator('[data-testid="voice-call-button"]')).toBeDisabled() + await expect(page.locator('[data-testid="video-call-button"]')).toBeDisabled() + + // Verify tooltips show why features are disabled + await page.locator('[data-testid="file-attachment-button"]').hover() + await expect(page.locator('[data-testid="feature-disabled-tooltip"]')).toBeVisible() + await expect(page.locator('[data-testid="feature-disabled-tooltip"]')).toContainText('ENS verification required') + }) + + test('should show basic messaging functionality only', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[1].address) // Bob (verified) + + // Verify only basic message input is available + await expect(page.locator('[data-testid="message-input"]')).toBeVisible() + await expect(page.locator('[data-testid="send-button"]')).toBeVisible() + + // Verify other input options are not available + await expect(page.locator('[data-testid="emoji-picker-button"]')).not.toBeVisible() + await expect(page.locator('[data-testid="sticker-button"]')).not.toBeVisible() + }) + + test('should handle search with limited results for unverified users', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Open search functionality + await page.locator('[data-testid="search-input"]').click() + + // Search for users + await page.locator('[data-testid="search-input"]').fill('user') + + // Verify search results don't show verification badges for current user + const searchResults = page.locator('[data-testid="search-results"] [data-testid="user-item"]') + const resultCount = await searchResults.count() + + for (let i = 0; i < resultCount; i++) { + const result = searchResults.nth(i) + const isCurrentUser = await result.getAttribute('data-is-current-user') + if (isCurrentUser === 'true') { + await expect(result.locator('[data-testid="verification-badge"]')).not.toBeVisible() + } + } + }) + + test('should display appropriate messaging limits for unverified users', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[0].address) // Alice + + // Send multiple messages to test rate limiting + for (let i = 1; i <= 5; i++) { + await sendMessage(`Test message ${i} from unverified user`) + + // After 3 messages, expect rate limiting warning + if (i === 3) { + await expect(page.locator('[data-testid="rate-limit-warning"]')).toBeVisible() + await expect(page.locator('[data-testid="rate-limit-warning"]')).toContainText('Verify ENS to remove message limits') + } + } + }) + + test('should show verification process in chat interface', async ({ + page, + waitForChatLoad + }) => { + await waitForChatLoad() + + // Verify verification prompt is visible in main interface + await expect(page.locator('[data-testid="verification-prompt-banner"]')).toBeVisible() + await expect(page.locator('[data-testid="verification-prompt-banner"]')).toContainText('Complete ENS verification to unlock all features') + + // Verify verification button is available + await expect(page.locator('[data-testid="start-verification-button"]')).toBeVisible() + }) + + test('should maintain messaging capability but show limitations', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[1].address) // Bob + + // Send message and verify it works + const messageContent = 'Basic message still works for unverified users' + await sendMessage(messageContent) + await verifyMessageDelivery(messageContent) + + // But verify limitations are clearly indicated + await expect(page.locator('[data-testid="feature-restriction-notice"]')).toBeVisible() + await expect(page.locator('[data-testid="feature-restriction-notice"]')).toContainText('Limited features - Verify ENS for full access') + }) + + test('should handle messaging with other unverified users', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + // Mock another unverified user + const otherUnverifiedUser = { + ...unverifiedUser, + address: '0x1111111111111111111111111111111111111111', + displayName: 'Another User' + } + + // Create a new chat with another unverified user (simulated) + await page.locator('[data-testid="add-contact-button"]').click() + await page.locator('[data-testid="user-search-input"]').fill(otherUnverifiedUser.address) + + // Verify no verification badges between unverified users + const userResult = page.locator('[data-testid="user-search-result"]').first() + await expect(userResult.locator('[data-testid="verification-badge"]')).not.toBeVisible() + }) +}) \ No newline at end of file diff --git a/next-frontend/e2e/tests/security/chat-security.spec.ts b/next-frontend/e2e/tests/security/chat-security.spec.ts new file mode 100644 index 0000000..83d2181 --- /dev/null +++ b/next-frontend/e2e/tests/security/chat-security.spec.ts @@ -0,0 +1,540 @@ +/** + * E2E Tests for Security Protections on Chat Routes + * + * These tests verify security measures including: + * - Route access control and authentication + * - Input validation and sanitization + * - CSRF protection + * - Message encryption and privacy + * - Rate limiting and abuse prevention + */ + +import { test, expect } from '../../fixtures/chat-fixtures' +import { MOCK_USERS } from '../../fixtures/chat-fixtures' + +test.describe('Chat Route Security Protections', () => { + test.beforeEach(async ({ page, mockWalletConnection, setupMockContract }) => { + const user = MOCK_USERS[0] // Alice - verified user + await mockWalletConnection(user) + await setupMockContract() + await page.goto('/') + }) + + test.describe('Route Access Control', () => { + test('should prevent unauthorized access to protected routes', async ({ + page + }) => { + // Start with no wallet connection + await page.context().clearCookies() + await page.goto('/') + + // Try to access protected chat route directly + await page.goto('/chat') + + // Verify redirect to authentication + await expect(page).toHaveURL(/.*\/.*auth.*/) + await expect(page.locator('[data-testid="authentication-required"]')).toBeVisible() + + // Try to access API routes directly + await page.goto('/api/chat/messages') + + // Should return unauthorized or redirect + const response = await page.request.get('/api/chat/messages') + expect(response.status()).toBe(401) + }) + + test('should validate wallet ownership for route access', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] // Alice + + // Connect with user's wallet + await mockWalletConnection(user) + await page.goto('/') + + // Try to access another user's private chat route + await page.goto(`/chat/private/${MOCK_USERS[1].address}`) + + // Verify access is granted (should be able to chat with others) + await expect(page.locator('[data-testid="chat-header"]')).toBeVisible() + + // Try to access invalid/inaccessible route + await page.goto('/chat/private/invalid-address') + + // Should handle invalid route gracefully + await expect(page.locator('[data-testid="invalid-route-error"]')).toBeVisible() + }) + + test('should enforce authentication on all chat operations', async ({ + page, + mockWalletConnection + }) => { + // Clear authentication + await page.context().clearCookies() + await page.goto('/') + + // Try to perform chat operations without authentication + await page.locator('[data-testid="message-input"]').fill('Test message') + + // Should not allow sending without auth + await page.locator('[data-testid="send-button"]').click() + + // Verify authentication requirement + await expect(page.locator('[data-testid="auth-required-modal"]')).toBeVisible() + await expect(page.locator('[data-testid="auth-required-modal"]')).toContainText('Please connect your wallet') + }) + + test('should validate session tokens on protected routes', async ({ + page, + mockWalletConnection + }) => { + const user = MOCK_USERS[0] + + await mockWalletConnection(user) + await page.goto('/') + + // Obtain valid session token + const sessionToken = await page.evaluate(() => localStorage.getItem('sessionToken')) + + // Manually invalidate session + await page.evaluate(() => { + localStorage.setItem('sessionToken', 'invalid-token') + }) + + // Try to access protected API + const response = await page.request.get('/api/chat/send', { + headers: { + 'Authorization': 'Bearer invalid-token' + } + }) + + // Should reject invalid token + expect(response.status()).toBe(401) + + // Verify UI shows session expired + await expect(page.locator('[data-testid="session-expired-message"]')).toBeVisible() + }) + }) + + test.describe('Input Validation and Sanitization', () => { + test('should prevent XSS in message content', async ({ + page, + waitForChatLoad, + selectChat + }) => { + await waitForChatLoad() + + await selectChat(MOCK_USERS[1].address) // Bob + + // Attempt XSS injection + const xssPayload = '' + await page.locator('[data-testid="message-input"]').fill(xssPayload) + await page.locator('[data-testid="send-button"]').click() + + // Verify message is sanitized + const messageContent = await page.locator('[data-testid="message-bubble"]').textContent() + expect(messageContent).not.toContain('Test Group' + await page.locator('[data-testid="group-name-input"]').fill(maliciousName) + + // Verify sanitization + const sanitizedName = await page.locator('[data-testid="group-name-input"]').inputValue() + expect(sanitizedName).not.toContain('