diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000..099fa88 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,164 @@ +name: Auto Label Issues and PRs + +on: + issues: + types: [opened, edited] + pull_request: + types: [opened, edited, synchronize] + label: + types: [created, deleted] + +jobs: + label_issues: + runs-on: ubuntu-latest + if: github.event.action == 'opened' || github.event.action == 'edited' + + steps: + - name: Label enhancement issues + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue || context.payload.pull_request; + const title = issue.title.toLowerCase(); + const body = (issue.body || '').toLowerCase(); + const content = `${title} ${body}`; + + const labels = []; + + // Frontend labels + if (content.match(/\b(ui|ux|frontend|react|nextjs|component|interface|design|css|tailwind)\b/)) { + labels.push('frontend'); + } + + // Backend labels + if (content.match(/\b(api|backend|server|database|postgres|neon)\b/)) { + labels.push('backend'); + } + + // Smart contract labels + if (content.match(/\b(solidity|contract|smart|blockchain|foundry|whispr)\b/)) { + labels.push('smart-contracts'); + } + + // Documentation labels + if (content.match(/\b(doc|readme|guide|tutorial|documentation)\b/)) { + labels.push('documentation'); + } + + // Testing labels + if (content.match(/\b(test|testing|unit|integration|e2e|coverage)\b/)) { + labels.push('testing'); + } + + // Bug labels + if (content.match(/\b(bug|error|fix|broken|issue|problem)\b/)) { + labels.push('bug'); + } + + // Enhancement labels + if (content.match(/\b(feature|enhancement|improve|add|implement|support)\b/)) { + labels.push('enhancement'); + } + + // Help wanted labels + const hasHelpWantedKeywords = content.match(/\b(help|welcome|contribution|community)\b/); + const isEnhancement = labels.includes('enhancement') || labels.includes('documentation') || labels.includes('testing'); + const isBug = labels.includes('bug'); + + if (hasHelpWantedKeywords || (isEnhancement && !isBug)) { + labels.push('help wanted'); + } + + // Good first issue labels + const hasGoodFirstKeywords = content.match(/\b(simple|easy|beginner|starter|quick)\b/); + const isShortContent = content.length < 500; + const isDocOnly = labels.includes('documentation') && !labels.includes('smart-contracts') && !labels.includes('backend'); + + if ((hasGoodFirstKeywords || isDocOnly) && isShortContent && !content.match(/\b(complex|advanced|security)\b/)) { + labels.push('good first issue'); + } + + // Apply labels if any were identified + if (labels.length > 0) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: labels + }); + console.log(`Applied labels to ${issue.html_url}: ${labels.join(', ')}`); + } catch (error) { + console.log(`Error applying labels: ${error.message}`); + } + } + + welcome_new_contributors: + runs-on: ubuntu-latest + if: github.event.action == 'opened' + + steps: + - name: Welcome new contributors + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue || context.payload.pull_request; + const isPR = !!context.payload.pull_request; + const url = issue.html_url; + + if (context.actor !== context.repo.owner) { + const welcomeMessage = isPR + ? `🎉 Thanks for your contribution to BlockBelle! We'll review your pull request shortly.` + : `👋 Welcome to BlockBelle! Thanks for your interest in helping improve this project.`; + + const helpMessage = `📝 Looking for ways to help? Check out our ["help wanted" issues](${context.payload.repository.html_url}/labels/help%20wanted) and [good first issues](${context.payload.repository.html_url}/labels/good%20first%20issue)!`; + + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `${welcomeMessage}\n\n${helpMessage}\n\n💡 Don't forget to check out our [contribution guide](${context.payload.repository.html_url}/blob/main/docs/help-wanted-labels-guide.md) for more details!` + }); + } catch (error) { + console.log(`Error creating welcome comment: ${error.message}`); + } + } + + weekly_label_review: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + + steps: + - name: Review and suggest labels for old issues + uses: actions/github-script@v7 + with: + script: | + const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + since: oneWeekAgo, + per_page: 100 + }); + + for (const issue of issues) { + const hasHelpWanted = issue.labels.some(label => label.name === 'help wanted'); + const hasGoodFirst = issue.labels.some(label => label.name === 'good first issue'); + const hasEnhancement = issue.labels.some(label => label.name === 'enhancement'); + + // Suggest help wanted for unlabelled enhancements + if (!hasHelpWanted && hasEnhancement) { + console.log(`Suggesting 'help wanted' label for issue: ${issue.title}`); + + // Add a comment suggesting the label + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `💡 This issue looks like it could benefit from community help! Consider adding the \`help wanted\` label to attract contributors.` + }); + } + } 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/docs/PR_PREPARATION.md b/docs/PR_PREPARATION.md new file mode 100644 index 0000000..8a1142e --- /dev/null +++ b/docs/PR_PREPARATION.md @@ -0,0 +1,153 @@ +# Pull Request: Add Help Wanted Labels System + +## Summary + +This PR implements a comprehensive "help wanted" labeling system to encourage community contributions to the BlockBelle project. The system includes automated labeling, detailed documentation, and practical tools for maintaining contributor-friendly issues. + +## What This PR Does + +### 🎯 Core Implementation +- **Labels Framework**: 10 primary and secondary labels for categorizing issues +- **Issue Templates**: 3 pre-built templates for different contribution types +- **Automation Tools**: Scripts and GitHub Actions for label management +- **Documentation**: Complete guides for maintainers and contributors + +### 📋 Files Changed + +#### Documentation (New) +- `docs/help-wanted-analysis.md` - Comprehensive codebase analysis +- `docs/help-wanted-labels-guide.md` - Implementation and usage guide +- `docs/PR_PREPARATION.md` - This file + +#### Scripts (New) +- `scripts/labels-setup.sh` - Automated GitHub label creation +- `scripts/label-issues.py` - Smart issue labeling tool +- `scripts/requirements.txt` - Python dependencies + +#### GitHub Actions (New) +- `.github/workflows/auto-label.yml` - Automated labeling workflow + +## Immediate Action Items + +### 1. Review the Documentation +Read the following files to understand the complete system: +- `docs/help-wanted-analysis.md` - Understanding of project areas needing help +- `docs/help-wanted-labels-guide.md` - Implementation instructions +- This file - Next steps and timeline + +### 2. Create GitHub Labels +Use the automated script to create all necessary labels: + +```bash +# Make sure you have a GitHub token with repo permissions +export GITHUB_TOKEN="your_token_here" + +# Run the label setup script +./scripts/labels-setup.sh $GITHUB_TOKEN Ryjen1 BlockBelle +``` + +### 3. Apply Labels to Existing Issues +Run the issue analysis script to suggest labels: + +```bash +# Install dependencies +pip install -r scripts/requirements.txt + +# Analyze and suggest labels (manual mode) +python scripts/label-issues.py $GITHUB_TOKEN Ryjen1 BlockBelle + +# Or auto-apply all suggestions +python scripts/label-issues.py $GITHUB_TOKEN Ryjen1 BlockBelle --auto +``` + +### 4. Enable GitHub Actions +The workflow file is included and will automatically: +- Label new issues based on content analysis +- Welcome new contributors with helpful messages +- Suggest "help wanted" labels for unlabelled enhancement issues + +## Expected Timeline + +### Day 1: Setup (30 minutes) +- [ ] Create GitHub labels using automation script +- [ ] Test issue labeling on a few existing issues +- [ ] Verify GitHub Actions workflow is enabled + +### Day 2: Launch (15 minutes) +- [ ] Create 5-10 new issues using the provided templates +- [ ] Apply "help wanted" labels to appropriate existing issues +- [ ] Announce in project documentation/communication channels + +### Week 1: Monitor & Adjust +- [ ] Review automated labeling accuracy +- [ ] Adjust labeling rules if needed +- [ ] Track community engagement with labeled issues + +## Benefits for the Project + +### For Contributors +- **Clear Entry Points**: Good first issues for newcomers +- **Detailed Guidance**: Templates ensure well-described tasks +- **Automated Support**: Welcome messages and helpful comments + +### for Maintainers +- **Organized Issues**: Systematic labeling improves project management +- **Reduced Friction**: Automated workflows handle routine tasks +- **Community Growth**: Clear pathways for new contributors + +### For the Project +- **Increased Contributions**: Better discoverability of contribution opportunities +- **Quality Improvement**: More community eyes lead to better code +- **Sustainable Growth**: Systems that support long-term contributor engagement + +## Monitoring Success + +### Key Metrics to Track +1. **New Contributors**: Number of first-time contributors per month +2. **Help Wanted Issues**: Ratio of labeled to unlabeled enhancement issues +3. **Issue Resolution**: Time to close "good first issues" +4. **Community Engagement**: Comments and discussions on labeled issues + +### Monthly Reviews +- Review labeling accuracy and adjust rules +- Archive completed "help wanted" issues +- Add new issues based on project evolution +- Update documentation based on community feedback + +## Support & Resources + +### For Contributors +- **Getting Started**: Check `docs/help-wanted-analysis.md` for beginner-friendly tasks +- **Contribution Guide**: Refer to issue templates for detailed requirements +- **Help Available**: All labeled issues welcome community questions and discussions + +### For Maintainers +- **Label Management**: Use `scripts/labels-setup.sh` for consistent labeling +- **Issue Creation**: Template files ensure quality issue descriptions +- **Automation**: GitHub Actions handle routine labeling tasks + +## Rollback Plan + +If needed, the system can be easily removed: +1. Delete GitHub labels through repository settings +2. Remove `.github/workflows/auto-label.yml` +3. Archive or delete the documentation files +4. The scripts are standalone and don't affect core functionality + +## Next Steps After Merge + +1. **Immediate**: Follow the "Immediate Action Items" section above +2. **Week 1**: Monitor automated labeling and community response +3. **Month 1**: Evaluate metrics and adjust the system as needed +4. **Ongoing**: Keep the system current with project evolution + +## Questions or Issues? + +If you encounter any problems with this implementation: +1. Check the troubleshooting section in `docs/help-wanted-labels-guide.md` +2. Review the automation scripts for error handling +3. Consider temporary manual labeling while troubleshooting + +--- + +**Ready to build a more contributor-friendly community? Let's make BlockBelle accessible to everyone!** 🚀 diff --git a/docs/help-wanted-analysis.md b/docs/help-wanted-analysis.md new file mode 100644 index 0000000..c1bd20f --- /dev/null +++ b/docs/help-wanted-analysis.md @@ -0,0 +1,123 @@ +# Help Wanted Analysis - BlockBelle Project + +## Overview + +Based on analysis of the BlockBelle codebase, here are areas where "help wanted" labels would be beneficial for new contributors. + +## Identified Improvement Areas + +### 1. Frontend Enhancements (next-frontend/) + +**Priority: High** +- **UI/UX Improvements**: The chat interface could benefit from modern design patterns +- **Mobile Responsiveness**: Ensure all components work well on mobile devices +- **Dark Mode Enhancement**: Current theme toggle could be expanded with more themes +- **Notification System**: The notification service could be enhanced with more features +- **Loading States**: Add better loading indicators for API calls +- **Error Boundaries**: Improve error handling and user feedback + +### 2. Smart Contract Improvements (src/) + +**Priority: Medium** +- **Gas Optimization**: Review and optimize gas usage in WhisprChat.sol +- **Security Audits**: Additional security reviews for production readiness +- **Event Logging**: Enhance event logging for better analytics +- **Group Management**: Add more sophisticated group management features +- **Message Encryption**: Consider implementing end-to-end encryption +- **Multi-chain Support**: Extend beyond Celo to other EVM chains + +### 3. Database & Backend + +**Priority: Medium** +- **Database Performance**: Optimize queries and indexing +- **API Rate Limiting**: Implement rate limiting for API endpoints +- **Caching Strategy**: Add Redis or similar caching layer +- **Database Migrations**: Add proper migration system +- **Backup Strategy**: Implement automated database backups +- **Monitoring**: Add comprehensive logging and monitoring + +### 4. Testing & Documentation + +**Priority: High** +- **Unit Tests**: Increase test coverage for both frontend and contracts +- **Integration Tests**: Add end-to-end testing pipeline +- **API Documentation**: Improve API documentation with examples +- **Contributor Guide**: Create comprehensive onboarding guide +- **Code Examples**: Add more usage examples and tutorials +- **Video Tutorials**: Create video content for setup and usage + +### 5. DevOps & Deployment + +**Priority: Medium** +- **CI/CD Pipeline**: Enhance testing and deployment automation +- **Environment Management**: Better environment variable management +- **Performance Monitoring**: Add performance metrics and alerting +- **Security Scanning**: Implement automated security checks +- **Docker Support**: Add Docker containerization +- **Kubernetes Deployment**: Support for scalable cloud deployment + +### 6. Feature Enhancements + +**Priority: Medium** +- **Voice Messages**: Support for audio messages +- **File Sharing**: Allow users to share files and images +- **Message Reactions**: Add emoji reactions to messages +- **Typing Indicators**: Show when users are typing +- **Message Search**: Implement full-text search functionality +- **Offline Support**: Add offline functionality with sync + +## Recommended Issue Templates + +### Good First Issue Template +``` +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. +``` + +### Help Wanted Template +``` +**Is this issue related to enhancement or bug?** +Please describe the issue. + +**What needs to be done?** +- [ ] Task 1 +- [ ] Task 2 +- [ ] Task 3 + +**Difficulty Level** +Easy | Medium | Hard + +**Expected Time** +1-2 hours | 1 day | 1 week + +**Skills Required** +List the skills needed to complete this task. + +**Additional Context** +Any additional information that would help contributors get started. +``` + +## Contribution Workflow + +1. **Good First Issues**: Label issues that are simple fixes or small features +2. **Help Wanted**: Label more complex features that need multiple contributors +3. **Good Documentation**: Ensure all issues have clear descriptions and requirements +4. **Mentorship**: Pair new contributors with experienced team members +5. **Recognition**: Highlight outstanding contributions in release notes + +## Next Steps + +1. Create GitHub issues for each identified area +2. Add appropriate labels ("good first issue", "help wanted", "enhancement", etc.) +3. Write detailed issue descriptions with acceptance criteria +4. Create contribution guides for each major area +5. Set up automated labeling for common patterns diff --git a/docs/help-wanted-labels-guide.md b/docs/help-wanted-labels-guide.md new file mode 100644 index 0000000..fabfa81 --- /dev/null +++ b/docs/help-wanted-labels-guide.md @@ -0,0 +1,271 @@ +# Help Wanted Labels Implementation Guide + +## Overview + +This guide provides the framework for implementing "help wanted" labels in the BlockBelle project to encourage community contributions. + +## GitHub Labels to Create + +### Primary Labels + +1. **`help wanted`** (Yellow) + - Use for issues where we want community help + - Should be applied to enhancement and non-critical bug issues + +2. **`good first issue`** (Green) + - Use for beginner-friendly tasks + - Simple fixes, documentation, small features + - Good for newcomers to the project + +3. **`enhancement`** (Blue) + - Use for feature requests and improvements + - Can be combined with "help wanted" + +4. **`documentation`** (Purple) + - Use for documentation improvements + - Easy entry point for contributors + +5. **`frontend`** (Orange) + - Use for Next.js/React related issues + - Easy to identify for frontend contributors + +6. **`smart-contracts`** (Red) + - Use for Solidity/Foundry related issues + - For blockchain development contributions + +### Secondary Labels + +7. **`testing`** (Pink) + - Use for test-related improvements + - Unit tests, integration tests, e2e tests + +8. **`ui/ux`** (Teal) + - Use for user interface and experience improvements + - Design-related tasks + +9. **`bug`** (Red) + - Use for bug reports (not critical ones) + +10. **`performance`** (Gray) + - Use for performance optimization tasks + +## Issue Templates + +### Template 1: Good First Issue + +```markdown +--- +name: Good First Issue +about: Suggest a task suitable for new contributors +title: '[FEATURE] Add feature for beginners' +labels: 'good first issue, help wanted' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Acceptance Criteria** +- [ ] Criteria 1 +- [ ] Criteria 2 +- [ ] Criteria 3 + +**Additional context** +Add any other context, screenshots, or helpful links about the feature request here. + +**Skills Required** +- Basic JavaScript/TypeScript +- Git/GitHub basics +- Beginner-level React knowledge + +**Estimated Time** +1-3 hours + +**Help Available** +Feel free to ask questions in the comments or join our community chat! +``` + +### Template 2: Help Wanted Enhancement + +```markdown +--- +name: Help Wanted Enhancement +about: Suggest an enhancement that needs community support +title: '[FEATURE] Enhance chat interface' +labels: 'help wanted, enhancement, frontend' +assignees: '' + +--- + +**Is this enhancement related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Acceptance Criteria** +- [ ] Criteria 1 +- [ ] Criteria 2 +- [ ] Criteria 3 + +**Technical Requirements** +- Files to modify: `next-frontend/src/components/ChatInterface.tsx` +- Dependencies: None or specify +- Breaking changes: None + +**Skills Required** +- React/Next.js development +- TypeScript +- CSS/Tailwind styling +- Web3/Ethereum basics (optional) + +**Estimated Time** +1-2 days + +**Help Available** +- Project maintainers can provide guidance +- Technical discussions welcome +- Code review and feedback guaranteed +``` + +### Template 3: Documentation Improvement + +```markdown +--- +name: Documentation Improvement +about: Improve project documentation +title: '[DOCS] Add setup guide for local development' +labels: 'documentation, help wanted, good first issue' +assignees: '' + +--- + +**What documentation needs improvement?** +Describe the current documentation issue or gap. + +**What would you like to see?** +A clear description of the documentation improvement needed. + +**Acceptance Criteria** +- [ ] Clear, step-by-step instructions +- [ ] Screenshots or code examples where helpful +- [ ] Proper formatting and structure +- [ ] Links to related resources + +**Files to Modify** +- `README.md` +- `next-frontend/LOCAL_DEVELOPMENT.md` +- Any other relevant files + +**Skills Required** +- Markdown formatting +- Clear writing +- Familiarity with the project setup + +**Estimated Time** +2-4 hours + +**Help Available** +Current documentation available for reference in the `docs/` directory. +``` + +## Implementation Instructions + +### Step 1: Create GitHub Labels + +Go to your repository settings and create the following labels: + +1. Click on "Issues" in the left sidebar +2. Click on "Labels" +3. Create each label with appropriate color: + - `help wanted` - Yellow (#fbca04) + - `good first issue` - Green (#00ff00) + - `enhancement` - Blue (#0366d6) + - `documentation` - Purple (#bfd4f2) + - `frontend` - Orange (#fb8500) + - `smart-contracts` - Red (#d73a4a) + - `testing` - Pink (#d876e3) + - `ui/ux` - Teal (#39d353) + - `bug` - Red (#d73a4a) + - `performance` - Gray (#6a737d) + +### Step 2: Create Issue Templates + +1. Go to `.github/ISSUE_TEMPLATE/` directory +2. Create the three markdown files with the templates above + +### Step 3: Apply Labels to Existing Issues + +1. Review existing issues in the repository +2. Apply appropriate labels: + - Enhancement issues → `enhancement` + `help wanted` + - Documentation issues → `documentation` + - Frontend improvements → `frontend` + `help wanted` + - Simple fixes → `good first issue` + +### Step 4: Create Specific Issues + +Based on the analysis in `docs/help-wanted-analysis.md`, create these issues: + +#### Good First Issues +- [ ] Add loading spinner to chat interface +- [ ] Improve error message formatting +- [ ] Add dark mode toggle help text +- [ ] Update README with setup screenshots +- [ ] Add code comments to smart contract functions + +#### Help Wanted Issues +- [ ] Implement mobile responsive design +- [ ] Add message search functionality +- [ ] Enhance notification system +- [ ] Optimize smart contract gas usage +- [ ] Add end-to-end tests + +#### Documentation Issues +- [ ] Create contributor guide +- [ ] Add API documentation +- [ ] Write deployment guide +- [ ] Create video tutorials outline + +## Maintenance + +### Regular Reviews +- Monthly review of labeled issues +- Archive completed "help wanted" issues +- Add new issues as project evolves + +### Community Engagement +- Respond to contributor questions promptly +- Provide guidance and mentorship +- Celebrate successful contributions + +### Metrics +- Track number of "help wanted" issues created +- Monitor contribution from labeled issues +- Measure time to resolution for different labels + +## Benefits + +1. **Increased Contributions**: Clear guidance attracts new contributors +2. **Better Issue Management**: Organized labeling system +3. **Mentorship Opportunities**: Easier to pair newcomers with experts +4. **Project Growth**: Sustainable community-driven development +5. **Quality Improvement**: More eyes on the code leads to better quality + +## Success Metrics + +- Track new contributors joining the project +- Monitor completion rate of "good first issues" +- Measure community engagement levels +- Assess code quality improvements from contributions +- Monitor issue resolution times 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/MUTE_FEATURE_README.md b/next-frontend/MUTE_FEATURE_README.md new file mode 100644 index 0000000..32db732 --- /dev/null +++ b/next-frontend/MUTE_FEATURE_README.md @@ -0,0 +1,219 @@ +# Mute Feature Implementation + +This document describes the implementation of the mute feature for individual users and group chats in the BlockBelle chat application. + +## Overview + +The mute feature allows users to silence notifications and messages from specific users or group chats without blocking them entirely. When a user or group is muted: + +- Push notifications are suppressed +- Messages still appear in the chat but don't generate notifications +- Users can easily unmute at any time +- Mute settings are persisted in the database + +## Architecture + +### Database Schema + +The mute functionality uses a new `mute_settings` table with the following structure: + +```sql +CREATE TABLE mute_settings ( + id SERIAL PRIMARY KEY, + user_address VARCHAR(42) NOT NULL, -- User who set the mute + muted_user_address VARCHAR(42) NULL, -- User being muted (NULL for groups) + muted_group_id VARCHAR(255) NULL, -- Group being muted (NULL for users) + mute_type VARCHAR(20) NOT NULL CHECK (mute_type IN ('user', 'group')), + muted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_muted BOOLEAN DEFAULT TRUE, + UNIQUE(user_address, muted_user_address, muted_group_id, mute_type) +); +``` + +### API Endpoints + +#### POST /api/mute +Toggle mute status for users or groups. + +**Request:** +```json +{ + "userAddress": "0x1234567890123456789012345678901234567890", + "targetUserAddress": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", // Optional for groups + "targetGroupId": "group123", // Optional for users + "mute": true, + "muteType": "user" // or "group" +} +``` + +#### GET /api/mute/check +Check if a specific user/group is muted. + +**Query Parameters:** +- `userAddress`: Required - user's address +- `targetUserAddress`: Optional - user to check (mutual exclusive with targetGroupId) +- `targetGroupId`: Optional - group to check (mutual exclusive with targetUserAddress) + +#### GET /api/mute/list +Get all mute settings for a user. + +**Query Parameters:** +- `userAddress`: Required - user's address +- `muteType`: Optional filter - "user" or "group" + +### Frontend Components + +#### useMute Hook +Main hook for managing mute state across the application. + +```typescript +const { + muteSettings, + isUserMuted, + isGroupMuted, + toggleUserMute, + toggleGroupMute, + refreshMuteSettings, + isLoading, + error +} = useMute(); +``` + +#### MuteButton Component +Reusable button component for muting users and groups. + +```typescript + console.log('Mute state changed:', isMuted)} +/> +``` + +#### MutedList Component +Component for managing all muted users and groups. + +```typescript + setShowMutedList(false)} + className="w-80" +/> +``` + +### Integration Points + +#### Notification Context +The `NotificationContext` now respects mute settings: + +```typescript +// Notifications are only shown if the sender/group is not muted +showNewMessageNotification( + senderName, + message, + chatType, + chatId, + senderAddress // Used for mute checking +); +``` + +#### Chat Components +Mute buttons are integrated into: +- `MainChat` - Chat header with mute controls +- `ChatInterface` - Individual chat mute functionality +- `GroupChat` - Group chat mute controls + +## Usage Examples + +### Muting a User +```typescript +const { toggleUserMute } = useMute(); + +// Mute a user +await toggleUserMute('0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'); + +// Check if user is muted +const isMuted = isUserMuted('0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'); +``` + +### Muting a Group +```typescript +const { toggleGroupMute } = useMute(); + +// Mute a group +await toggleGroupMute('group123'); + +// Check if group is muted +const isMuted = isGroupMuted('group123'); +``` + +### Using MuteButton Component +```typescript +// In a chat header +
+ +
+ +// In a group chat + +``` + +## State Management + +The mute functionality uses optimistic updates for better UX: + +1. **Local State Update**: UI updates immediately when mute is toggled +2. **API Call**: Backend call to persist the change +3. **State Refresh**: Complete settings are refreshed from the server +4. **Error Handling**: If the API call fails, the UI reverts to the previous state + +## Error Handling + +- Network errors are caught and displayed to users +- Invalid Ethereum addresses are validated +- Permission checks ensure only the mute owner can modify settings +- Loading states prevent duplicate requests + +## Testing + +Comprehensive test coverage includes: + +- Unit tests for `useMute` hook +- Component tests for `MuteButton` +- API endpoint testing with mocked responses +- Integration tests for notification suppression + +## Performance Considerations + +- Mute settings are cached locally and refreshed periodically +- Database queries use proper indexes for fast lookups +- UI updates use optimistic patterns for responsiveness +- Component memoization prevents unnecessary re-renders + +## Security + +- All API endpoints validate Ethereum addresses +- Users can only modify their own mute settings +- SQL injection prevention through parameterized queries +- Input validation on all user-provided data + +## Future Enhancements + +Potential improvements: +- Mute duration (temporary mutes) +- Granular notification preferences (sound, visual, etc.) +- Bulk mute operations +- Mute analytics and insights +- Integration with message filtering +- Mobile app push notification suppression \ 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('