From 452c14a57da44a13b2add1dcf78681693646d532 Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Tue, 2 Dec 2025 06:30:11 +0100 Subject: [PATCH 01/11] feat: add Playwright E2E testing framework configuration --- next-frontend/e2e/README.md | 347 +++++++++++ next-frontend/e2e/fixtures/chat-fixtures.ts | 262 +++++++++ .../e2e/tests/auth/login-logout.spec.ts | 502 ++++++++++++++++ .../e2e/tests/chat/ens-verified-chat.spec.ts | 213 +++++++ .../e2e/tests/chat/group-chat.spec.ts | 477 ++++++++++++++++ .../e2e/tests/chat/message-delivery.spec.ts | 373 ++++++++++++ .../e2e/tests/chat/non-verified-chat.spec.ts | 244 ++++++++ .../e2e/tests/security/chat-security.spec.ts | 540 ++++++++++++++++++ next-frontend/package.json | 8 +- next-frontend/playwright.config.ts | 50 ++ 10 files changed, 3015 insertions(+), 1 deletion(-) create mode 100644 next-frontend/e2e/README.md create mode 100644 next-frontend/e2e/fixtures/chat-fixtures.ts create mode 100644 next-frontend/e2e/tests/auth/login-logout.spec.ts create mode 100644 next-frontend/e2e/tests/chat/ens-verified-chat.spec.ts create mode 100644 next-frontend/e2e/tests/chat/group-chat.spec.ts create mode 100644 next-frontend/e2e/tests/chat/message-delivery.spec.ts create mode 100644 next-frontend/e2e/tests/chat/non-verified-chat.spec.ts create mode 100644 next-frontend/e2e/tests/security/chat-security.spec.ts create mode 100644 next-frontend/playwright.config.ts 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/tests/auth/login-logout.spec.ts b/next-frontend/e2e/tests/auth/login-logout.spec.ts new file mode 100644 index 0000000..53da330 --- /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..ae76333 --- /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..bfa06e7 --- /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..e8e9806 --- /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..c371bdd --- /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..73bcc1a --- /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('