diff --git a/.ai/README.md b/.ai/README.md new file mode 100644 index 0000000000..4d350f8ffe --- /dev/null +++ b/.ai/README.md @@ -0,0 +1,190 @@ +# RedisInsight AI Development Rules + +This directory contains the **single source of truth** for AI-assisted development rules and workflows in RedisInsight. These rules are used by multiple AI coding assistants: + +- **Cursor** (via symlinks: `.cursor/rules/` and `.cursor/commands/`) +- **Augment** (via symlink: `.augment/`) +- **Windsurf** (via symlink: `.windsurfrules`) +- **GitHub Copilot** (via file: `.github/copilot-instructions.md`) + +## MCP (Model Context Protocol) Setup + +AI tools can access external services (JIRA, Confluence, GitHub) via MCP configuration. + +### Initial Setup + +1. **Copy the example configuration:** + + ```bash + cp env.mcp.example .env.mcp + ``` + +2. **Get your Atlassian API token:** + + - Go to: https://id.atlassian.com/manage-profile/security/api-tokens + - Create a classic token by pressing the first "Create Token" button + - Copy the token + +3. **Edit `.env.mcp` with your credentials:** + + ```bash + ATLASSIAN_DOMAIN=your-domain.atlassian.net + ATLASSIAN_EMAIL=your-email@example.com + ATLASSIAN_API_TOKEN=your-api-token-here + ATLASSIAN_JIRA_PROJECT=RI + ``` + +4. **Verify your setup:** + + **For Cursor users:** + + - Restart Cursor to load the new MCP configuration + - Ask the AI: "Can you list all available MCP tools and test them?" + - The AI should be able to access JIRA, Confluence, GitHub, and other configured services + + **For Augment users:** + + ```bash + npx @augmentcode/auggie --mcp-config mcp.json "go over all my mcp tools and make sure they work as expected" + ``` + + **For GitHub Copilot users:** + + - Note: GitHub Copilot does not currently support MCP integration + - MCP services (JIRA, Confluence, etc.) will not be available in Copilot + +### Available MCP Services + +The `mcp.json` file configures these services: + +- **git** - Git operations (status, diff, log, branch management) +- **github** - GitHub integration (issues, PRs, repository operations) +- **memory** - Persistent context storage across sessions +- **sequential-thinking** - Enhanced reasoning for complex tasks +- **context-7** - Advanced context management +- **atlassian** - JIRA (RI-XXX tickets) and Confluence integration + +**Note**: Never commit `.env.mcp` to version control (it's in `.gitignore`)! + +## Structure + +``` +.ai/ +├── README.md # This file +├── rules/ # Development rules and standards +│ ├── 01-CODE_QUALITY.md # Linting, formatting, TypeScript +│ ├── 02-FRONTEND.md # React, Redux, styled-components, component structure +│ ├── 03-BACKEND.md # NestJS, API patterns +│ ├── 04-TESTING.md # Testing standards and practices +│ └── 05-WORKFLOW.md # Git workflow, commits, dev process +└── commands/ # AI workflow commands + ├── pr/ + │ ├── review.md # PR review workflow + │ └── plan.md # PR planning workflow + ├── commit-message.md # Commit message generation + └── run-ui-tests.md # Custom test runner usage +``` + +## Project Overview + +**RedisInsight** is a desktop application for Redis database management built with: + +- **Frontend**: React 18, TypeScript, Redux Toolkit, Elastic UI, Monaco Editor, Vite +- **Backend**: NestJS, TypeScript, Node.js +- **Desktop**: Electron for cross-platform distribution +- **Testing**: Jest, Testing Library, Playwright + +**Architecture**: + +``` +redisinsight/ +├── ui/ # React frontend (Vite + TypeScript) +├── api/ # NestJS backend (TypeScript) +├── desktop/ # Electron main process +└── tests/ # E2E tests (Playwright) +``` + +## Quick Reference + +### Essential Commands + +```bash +# Development +yarn dev:ui # Start UI dev server +yarn dev:api # Start API dev server +yarn dev:desktop # Start full Electron app + +# Testing +yarn test # Run UI tests +yarn test:api # Run API tests +yarn test:cov # Run tests with coverage + +# Code Quality +yarn lint # Lint all code +yarn type-check:ui # TypeScript type checking +yarn prettier:fix # Fix formatting on the changed files +``` + +### Before Every Commit + +1. ✅ Run linter: `yarn lint` +2. ✅ Run tests: `yarn test && yarn test:api` +3. ✅ Check types: `yarn type-check:ui` +4. ✅ Fix formatting: `yarn prettier:fix` + +### Key Principles + +- **Quality over speed** - Write maintainable, testable code +- **Always run linter** after changes +- **No semicolons** in TypeScript files +- **Use styled-components** for styling (migrating from SCSS modules) +- **Use faker** for test data generation +- **Never use fixed timeouts** in tests +- **Test coverage** must meet thresholds (80% statements/lines) + +## Module Aliases + +- `uiSrc/*` → `redisinsight/ui/src/*` +- `apiSrc/*` → `redisinsight/api/src/*` +- `desktopSrc/*` → `redisinsight/desktop/src/*` + +## Redis-Specific Context + +- Support all Redis data types: String, Hash, List, Set, Sorted Set, Vector Set, Stream, JSON +- Handle Redis modules: RedisJSON, RediSearch, RedisTimeSeries, RedisGraph +- Connection types: Standalone, Cluster, Sentinel +- Features: Workbench, Data Browser, Profiler, SlowLog, Pub/Sub + +## For AI Assistants + +When helping with RedisInsight development: + +### DO: + +- ✅ Follow established patterns in the codebase +- ✅ Run linter and tests before suggesting code is complete +- ✅ Use proper TypeScript types (avoid `any`) +- ✅ Write tests for all new features +- ✅ Consider performance and accessibility +- ✅ Handle errors properly +- ✅ Reference relevant existing code + +### DON'T: + +- ❌ Use `console.log` +- ❌ Add unnecessary comments +- ❌ Ignore linting errors +- ❌ Skip tests +- ❌ Use deprecated APIs +- ❌ Mutate Redux state directly +- ❌ Use magic numbers or unclear names + +## Updating These Rules + +To update AI rules: + +1. **Edit files in `.ai/` only** (never edit symlinked files directly) +2. Changes automatically propagate to all AI tools +3. Commit changes to version control + +**Remember**: These rules exist to maintain code quality and consistency. Follow them, but also use good judgment. diff --git a/.ai/commands/commit-message.md b/.ai/commands/commit-message.md new file mode 100644 index 0000000000..74495788de --- /dev/null +++ b/.ai/commands/commit-message.md @@ -0,0 +1,413 @@ +# Commit Message Generation Guidelines + +## Purpose + +Generate concise, meaningful commit messages following RedisInsight conventions. + +## Commit Message Format + +Follow **Conventional Commits** format **WITH scope prefixes**: + +``` +(): + +[optional body] + +[optional footer] +``` + +## Types + +- `feat` - New feature +- `fix` - Bug fix (use `bugfix` for branch names) +- `refactor` - Code refactoring (no functional changes) +- `chore` - Maintenance tasks, dependencies +- `docs` - Documentation changes +- `test` - Adding or updating tests +- `style` - Code style changes (formatting, whitespace) +- `perf` - Performance improvements +- `ci` - CI/CD changes +- `build` - Build system changes + +## Scopes + +Use scopes to indicate which part of the codebase is affected: + +- `api` - Backend API changes +- `ui` - Frontend UI changes +- `e2e` - End-to-end tests +- `ci` - CI/CD configuration +- `deps` - Dependency updates + +## Rules + +### DO: + +- ✅ **Always include scope** in commit messages (e.g., `feat(api):`, `fix(ui):`) +- ✅ Keep messages **concise** (max 250 characters for subject line) +- ✅ Inspect **all uncommitted files** before generating +- ✅ Use `fix(scope):` if changes look like bug fixes +- ✅ Use `refactor(scope):` if changes are code improvements without functional changes +- ✅ Use `feat(scope):` if adding new functionality +- ✅ Provide commit message in **easy to copy format** +- ✅ Be specific about what changed +- ✅ Use imperative mood ("add feature" not "added feature") +- ✅ Start with lowercase after the scope + +### DON'T: + +- ❌ Don't omit scope - always include it +- ❌ Don't be overly verbose or too vague +- ❌ Don't include file names unless specifically relevant +- ❌ Don't use past tense +- ❌ Don't add period at the end of subject line +- ❌ Don't use multiple scopes in one commit (split into separate commits) + +## Examples + +### ✅ Good Commit Messages + +``` +feat(ui): add user profile editing interface + +fix(api): resolve memory leak in Redis connection pool + +refactor(api): extract validation logic to utility functions + +test(e2e): add tests for user authentication flow + +docs(api): update endpoint documentation + +chore(deps): upgrade React to version 18.2 + +perf(ui): optimize list rendering with virtualization + +style(ui): fix import order and formatting + +test(ui): add unit tests for Redux slices + +ci: update GitHub Actions workflow +``` + +### ❌ Bad Commit Messages + +``` +feat: add user profile editing interface ❌ Missing scope +Fix(ui): bug in component ❌ Capitalize type +added new feature ❌ Past tense, no type/scope +fix(ui): stuff ❌ Too vague +updated files ❌ Not descriptive, no scope +WIP ❌ Not meaningful +fix(ui): fixed the bug in UserProfile.tsx that was causing issues ❌ Too verbose +feat(ui,api): add feature ❌ Multiple scopes (split into commits) +``` + +## Multi-Line Commit Messages + +For complex changes, use the body to provide more context: + +``` +feat(api): add Redis cluster support + +- Implement connection pooling and automatic failover for Redis cluster configurations +- Cluster node discovery +- Connection retry logic +- Failover handling + +References: #123 +``` + +## Special Cases + +### Breaking Changes + +``` +feat(api): redesign authentication flow + +BREAKING CHANGE: Auth tokens now expire after 1 hour +instead of 24 hours. Users will need to re-authenticate +more frequently. + +References: #RI-123 +``` + +### Multiple Files in Same Scope + +When changes span multiple files in the same scope: + +``` +refactor(ui): restructure Redux store organization + +Reorganize slices for better separation of concerns +and improved maintainability. + +References: #RI-456 +``` + +### Bug Fixes with Issue References + +``` +fix(ui): prevent duplicate API calls on component mount + +Previously, useEffect was triggering twice due to +missing dependency array. + +Fixes #RI-789 +# or for GitHub issues: +Fixes #789 +``` + +### Multiple Scopes + +If changes affect multiple scopes, create separate commits: + +```bash +# ✅ Good: Separate commits +git commit -m "feat(api): add new endpoint for user data + +References: #RI-123" + +git commit -m "feat(ui): add UI for user data display + +References: #RI-123" + +# ❌ Bad: Combined in one commit +git commit -m "feat(api,ui): add user data feature" +``` + +### Referencing Issues in Commit Body + +Use these keywords to link commits to issues: + +**JIRA Tickets:** + +- `References: #RI-123` - Link to ticket +- `Fixes #RI-123` - Close ticket when merged +- `Closes #RI-123` - Close ticket when merged +- `Related to #RI-456` - Related but doesn't close + +**GitHub Issues:** + +- `Fixes #123` - Close issue when merged +- `Closes #123` - Close issue when merged +- `Resolves #123` - Close issue when merged +- `Related to #456` - Related but doesn't close + +**Multiple Issues:** + +``` +feat(ui): add bulk delete functionality + +Implements bulk operations for key deletion with +proper confirmation dialogs and progress tracking. + +Fixes #123 +Fixes #456 +Related to #RI-789 +``` + +## Process for Generating Commit Messages + +1. **Inspect all uncommitted files** + + ```bash + git status + git diff + ``` + +2. **Identify the scope**: + + - API changes? → `api` + - UI changes? → `ui` + - Tests? → `e2e` or scope of test + - Multiple scopes? → Create separate commits + +3. **Identify the type of change**: + + - New functionality? → `feat(scope):` + - Bug fix? → `fix(scope):` + - Code improvement? → `refactor(scope):` + - Maintenance? → `chore(scope):` + - Tests? → `test(scope):` + - Documentation? → `docs(scope):` + +4. **Identify issue/ticket reference**: + + - JIRA ticket? → Add `References: #RI-XXX` or `Fixes #RI-XXX` in body + - GitHub issue? → Add `Fixes #XXX` or `Closes #XXX` in body + - Both? → Include both references + +5. **Write clear description**: + + - What changed? + - Why was it changed? + - Keep subject under 250 characters + +6. **Provide in copyable format**: + + ``` + Copy this commit message: + + feat(ui): add dark mode toggle to settings + + Implements user-requested dark mode with system + preference detection and manual override. + + Fixes #123 + ``` + +## Tools Integration + +### Using JetBrains MCP (if available) + +When possible, use JetBrains MCP to inspect uncommitted changes: + +- Get list of modified files +- Analyze diffs +- Generate contextual commit message + +### Using Terminal + +Combine commands for efficiency: + +```bash +git status && git diff --stat && git diff +``` + +## Branch-Specific Considerations + +Remember that RedisInsight uses these branch naming conventions: + +### Internal Development (JIRA Tickets): + +- `feature/RI-XXX/` - New features +- `bugfix/RI-XXX/` - Bug fixes +- `fe/feature/RI-XXX/` - Frontend-only features +- `be/bugfix/RI-XXX/` - Backend-only fixes + +### Open Source Contributions (GitHub Issues): + +- `feature/XXX/` - New features +- `bugfix/XXX/` - Bug fixes +- `fe/feature/XXX/` - Frontend-only features +- `be/bugfix/XXX/` - Backend-only fixes + +Where: + +- `RI-XXX` is the JIRA ticket number (e.g., `RI-123`) - for internal development +- `XXX` is the GitHub issue number (e.g., `456`) - for open source contributions +- **Important**: Branch names don't use `#`, but commit message references do (e.g., `Fixes #456`) + +### Matching Branch to Commit: + +- `feature/RI-XXX/*` or `feature/XXX/*` → `feat(scope):` commits +- `bugfix/RI-XXX/*` or `bugfix/XXX/*` → `fix(scope):` commits +- `fe/feature/RI-XXX/*` or `fe/feature/XXX/*` → `feat(ui):` commits +- `be/bugfix/RI-XXX/*` or `be/bugfix/XXX/*` → `fix(api):` commits + +### Example Branch Names: + +```bash +# Internal development (JIRA) +feature/RI-123/add-user-profile +bugfix/RI-456/fix-memory-leak +fe/feature/RI-789/add-dark-mode +be/bugfix/RI-234/fix-redis-connection + +# Open source contributions (GitHub) +feature/123/add-export-feature +bugfix/456/fix-connection-timeout +fe/feature/789/improve-accessibility +be/bugfix/234/handle-edge-case +``` + +## Interactive Commit Message Generation + +When asked to generate a commit message: + +1. **Analyze changes** comprehensively +2. **Determine appropriate scope** (api/ui/desktop/etc.) +3. **Determine appropriate type** (feat/fix/refactor/etc.) +4. **Check for issue/ticket references** in branch name or context +5. **Craft clear, concise description** +6. **Present in easy-to-copy format**: + +Example output for internal development: + +```markdown +Based on the uncommitted changes, here's your commit message: + +\`\`\` +feat(api): add user authentication with OAuth 2.0 + +Implements OAuth 2.0 authentication flow with token +management and refresh token support. + +References: #RI-123 +\`\`\` + +This covers the addition of OAuth integration, login flow, +and token management across 8 modified API files. +``` + +Example output for open source contribution: + +```markdown +Based on the uncommitted changes, here's your commit message: + +\`\`\` +fix(ui): prevent memory leak in connection monitor + +Properly cleanup subscriptions and timers when +component unmounts. + +Fixes #456 +\`\`\` + +This resolves the memory leak issue reported in GitHub. +``` + +If changes span multiple scopes: + +```markdown +I notice changes in both API and UI. I recommend two commits: + +**Commit 1 (API changes):** +\`\`\` +feat(api): add OAuth 2.0 authentication endpoints + +References: #RI-123 +\`\`\` + +**Commit 2 (UI changes):** +\`\`\` +feat(ui): add OAuth login interface + +References: #RI-123 +\`\`\` +``` + +## Quick Reference + +| Type | Scope | When to Use | Example | +| -------- | -------------- | ---------------- | ------------------------------------------ | +| feat | api/ui/desktop | New feature | `feat(ui): add export to PDF` | +| fix | api/ui/desktop | Bug fix | `fix(ui): correct tooltip position` | +| refactor | api/ui/desktop | Code improvement | `refactor(api): simplify validation logic` | +| chore | deps/config | Maintenance | `chore(deps): update dependencies` | +| docs | api/ui | Documentation | `docs(api): add endpoint examples` | +| test | e2e/ui/api | Tests | `test(e2e): add integration tests` | +| style | ui/api | Formatting | `style(ui): fix indentation` | +| perf | ui/api | Performance | `perf(api): cache responses` | +| ci | - | CI/CD | `ci: update GitHub Actions` | + +## Remember + +- **Always include scope** - Makes it clear what part of the codebase changed +- **Reference issues/tickets** - Use `Fixes #RI-XXX` or `Fixes #XXX` in commit body +- **Quality over speed** - Take time to write meaningful messages +- **Future developers** will read these - make them helpful +- **Git history** is documentation - keep it clean and clear +- **One scope per commit** - Split multi-scope changes into separate commits +- **Match branch type** - `bugfix/*` → `fix(scope):`, `feature/*` → `feat(scope):` +- **Support both tracking systems** - JIRA (#RI-XXX) for internal, GitHub (#XXX) for open source diff --git a/.ai/commands/pr/plan.md b/.ai/commands/pr/plan.md new file mode 100644 index 0000000000..17e66c9e50 --- /dev/null +++ b/.ai/commands/pr/plan.md @@ -0,0 +1,417 @@ +# PR Plan Command + +## Purpose + +Analyzes a JIRA ticket and creates a detailed implementation plan for a feature or bug fix. + +## Usage + +```bash +/pr:plan +``` + +## Prerequisites + +1. JIRA ticket must exist and be accessible +2. JIRA MCP tool configured +3. Ticket has clear requirements and acceptance criteria +4. Ticket scope is small and manageable + +## Process + +### 1. Ticket Validation + +Verify ticket quality: + +- [ ] Title is clear and descriptive +- [ ] Description is detailed +- [ ] Acceptance criteria defined +- [ ] Requirements are testable +- [ ] Scope is reasonable (can be completed in reasonable time) +- [ ] Dependencies are identified +- [ ] No blockers present + +If ticket quality is insufficient, provide feedback on what needs improvement. + +### 2. Codebase Analysis + +Understand current implementation: + +- Identify affected modules and files +- Review related code and patterns +- Understand data models and flow +- Identify dependencies and impacts +- Review existing tests +- Check for similar implementations + +### 3. Implementation Planning + +Create a detailed plan with: + +- **Overview**: High-level description of changes +- **Phases**: Break down into manageable steps +- **Files to Create/Modify**: List all affected files +- **Technical Approach**: Architecture and design decisions +- **Data Model Changes**: Database/state modifications +- **API Changes**: New or modified endpoints +- **UI Changes**: Component and state management updates +- **Testing Strategy**: Unit, integration, and E2E tests +- **Risks and Considerations**: Potential issues and mitigation + +### 4. Risk Assessment + +Identify potential risks: + +- Breaking changes +- Performance impacts +- Security concerns +- Backward compatibility +- Data migration needs +- Third-party dependencies + +### 5. Testing Strategy + +Define comprehensive testing: + +- Unit tests for new/modified functions +- Integration tests for API endpoints +- Component tests for React components +- E2E tests for critical user flows +- Edge case scenarios +- Error handling tests + +## Implementation Plan Template + +````markdown +# Implementation Plan: [TICKET-ID] - [Title] + +**Date**: YYYY-MM-DD +**Estimated Effort**: X hours/days +**Complexity**: Low/Medium/High +**Risk Level**: Low/Medium/High + +## Overview + +Brief description of what needs to be implemented and why. + +## Ticket Quality Assessment + +- ✅/❌ Clear requirements +- ✅/❌ Defined acceptance criteria +- ✅/❌ Reasonable scope +- ✅/❌ No blockers +- ✅/❌ Dependencies identified + +## Current State Analysis + +Description of current implementation and what needs to change. + +### Affected Modules + +- Frontend: [List modules] +- Backend: [List modules] +- Shared: [List modules] + +### Related Files + +- `path/to/file1.ts` - Description +- `path/to/file2.tsx` - Description + +## Implementation Phases + +### Phase 1: Backend API + +1. Create DTOs for validation + + - File: `redisinsight/api/src/feature/dto/create-feature.dto.ts` + - Add validation decorators + - Define proper types + +2. Implement service layer + + - File: `redisinsight/api/src/feature/feature.service.ts` + - Business logic implementation + - Error handling + +3. Create controller endpoints + + - File: `redisinsight/api/src/feature/feature.controller.ts` + - REST endpoints + - Swagger documentation + +4. Add unit tests + - File: `redisinsight/api/src/feature/feature.service.spec.ts` + - Test all service methods + - Mock dependencies + +### Phase 2: Frontend State Management + +1. Create Redux slice + + - File: `redisinsight/ui/src/slices/feature/feature.ts` + - Define state interface + - Create reducers and actions + +2. Implement selectors + + - File: `redisinsight/ui/src/slices/feature/selectors.ts` + - Memoized selectors + - Derived state + +3. Create thunks for API calls + - File: `redisinsight/ui/src/slices/feature/thunks.ts` + - Async actions + - Error handling + +### Phase 3: Frontend UI Components + +1. Create component folder structure + + - Folder: `redisinsight/ui/src/components/Feature/` + - Follow component guidelines structure: + - `Feature.tsx` - Main component + - `Feature.spec.tsx` - Component tests + - `Feature.styles.ts` - Styled components + - `Feature.types.ts` - Type definitions + - `index.ts` - Barrel file (if 3+ exports) + +2. Implement styled components + + - File: `redisinsight/ui/src/components/Feature/Feature.styles.ts` + - Use styled-components + - Export styled elements (Container, Title, etc.) + - Follow naming: Styled prefix (e.g., StyledContainer) + +3. Create main feature component + + - File: `redisinsight/ui/src/components/Feature/Feature.tsx` + - Functional component with TypeScript + - Props interface in Feature.types.ts + - State integration with Redux hooks + - Import styled components + +4. Create sub-components (if needed) + + - Folder: `redisinsight/ui/src/components/Feature/components/` + - Each sub-component follows same structure + - Keep component-specific utilities in Feature/utils/ + - Keep component-specific hooks in Feature/hooks/ + +5. Add component tests + - File: `redisinsight/ui/src/components/Feature/Feature.spec.tsx` + - Render tests + - Interaction tests + - Use Testing Library + - Use faker for test data + +### Phase 4: Integration & Testing + +1. Integration tests + + - File: `redisinsight/api/test/api/feature.e2e.spec.ts` + - Test API endpoints + - Test error scenarios + +2. E2E tests + + - File: `tests/e2e/tests/feature/feature.e2e.ts` + - Critical user flows + - Cross-browser testing + +3. Manual testing checklist + - [ ] Happy path flow + - [ ] Error scenarios + - [ ] Edge cases + - [ ] Performance + - [ ] Accessibility + +## Technical Approach + +### Data Model + +```typescript +interface Feature { + id: string; + name: string; + value: string; + createdAt: Date; + updatedAt: Date; +} +``` +```` + +### API Endpoints + +- `GET /api/feature` - List all features +- `GET /api/feature/:id` - Get feature by ID +- `POST /api/feature` - Create new feature +- `PUT /api/feature/:id` - Update feature +- `DELETE /api/feature/:id` - Delete feature + +### State Structure + +```typescript +interface FeatureState { + features: Feature[]; + loading: boolean; + error: string | null; + selectedId: string | null; +} +``` + +## Testing Strategy + +### Backend Tests + +1. **Unit Tests** (feature.service.spec.ts) + + - Test all service methods + - Mock database calls + - Test error handling + - Coverage: >80% + +2. **Integration Tests** (feature.e2e.spec.ts) + - Test API endpoints + - Test validation + - Test database persistence + - Test error responses + +### Frontend Tests + +1. **Unit Tests** (thunks.spec.ts, selectors.spec.ts) + + - Test Redux logic + - Test selectors + - Test async actions + - Coverage: >80% + +2. **Component Tests** (Feature.spec.tsx) + + - Test rendering + - Test user interactions + - Test error states + - Test loading states + - Use faker for data + - No fixed timeouts + +3. **E2E Tests** (feature.e2e.ts) + - Test complete user flow + - Test cross-component interaction + - Test real API integration + +## Risks and Considerations + +### Technical Risks + +1. **Performance Risk** (Medium) + + - Large dataset rendering + - Mitigation: Implement virtualization + +2. **Breaking Change Risk** (Low) + - New API endpoints only + - No modification to existing APIs + +### Dependencies + +- No external dependencies required +- Uses existing Redux patterns +- Uses existing UI components + +### Performance Impact + +- Minimal - new feature only +- Consider lazy loading for the module + +### Security Considerations + +- Input validation with class-validator +- Output sanitization in UI +- Authorization checks in API + +## Acceptance Criteria Mapping + +1. ✅ User can create new feature + + - Backend: POST endpoint + - Frontend: Form component + - Tests: Integration + E2E + +2. ✅ User can view features + + - Backend: GET endpoint + - Frontend: List component + - Tests: Component + E2E + +3. ✅ User can edit features + + - Backend: PUT endpoint + - Frontend: Edit form + - Tests: Integration + E2E + +4. ✅ User can delete features + - Backend: DELETE endpoint + - Frontend: Delete button + - Tests: Integration + E2E + +## Timeline Estimate + +| Phase | Duration | Notes | +| ------------------------- | ------------ | ------------------ | +| Phase 1: Backend | 4 hours | API + Tests | +| Phase 2: State Management | 3 hours | Redux + Tests | +| Phase 3: UI Components | 5 hours | Components + Tests | +| Phase 4: Integration | 2 hours | E2E + Manual | +| **Total** | **14 hours** | **~2 days** | + +## Definition of Done + +- [ ] All acceptance criteria met +- [ ] Unit test coverage >80% +- [ ] Integration tests passing +- [ ] E2E tests passing +- [ ] No linting errors +- [ ] Code reviewed +- [ ] Documentation updated +- [ ] Manual testing completed + +## Next Steps + +1. Review and approve this plan +2. Create feature branch +3. Implement Phase 1 +4. Review Phase 1 before moving to Phase 2 +5. Continue iteratively through phases +6. Final review and testing +7. Create PR for review + +``` + +## Plan Review Guidelines + +Before executing the plan: +1. **Review completeness**: All requirements covered? +2. **Check feasibility**: Is the approach practical? +3. **Verify estimates**: Are timelines realistic? +4. **Assess risks**: Are mitigation strategies adequate? +5. **Validate testing**: Is testing strategy comprehensive? + +## Execution Guidelines + +After plan approval: +1. Create feature branch +2. Implement phase by phase +3. Commit after each logical unit +4. Run tests continuously +5. Run linter frequently +6. Update plan if needed +7. Document deviations from plan + +## Notes +- Keep ticket scope small for better planning +- Break large features into multiple tickets +- Update plan if requirements change +- Document technical decisions +- Get clarification on ambiguous requirements + +``` diff --git a/.ai/commands/pr/review.md b/.ai/commands/pr/review.md new file mode 100644 index 0000000000..884d180e82 --- /dev/null +++ b/.ai/commands/pr/review.md @@ -0,0 +1,239 @@ +# PR Review Command + +## Purpose + +Reviews code changes in the current branch against requirements, best practices, and project standards. + +## Usage + +```bash +/pr:review +``` + +**Examples**: + +- JIRA ticket: `/pr:review RI-1234` +- GitHub issue: `/pr:review 456` + +## Prerequisites + +1. Checkout the branch to review locally +2. Ensure the ticket ID is valid and accessible +3. Have JIRA MCP tool configured (if using JIRA integration) + +## Process + +### 1. Gather Context + +- Fetch JIRA ticket details (if available) +- Read requirements and acceptance criteria +- Identify affected files in the PR +- Review recent commits + +### 2. Code Analysis + +Analyze the changes against: + +- **Code Quality**: Linting rules, TypeScript types, complexity +- **Testing**: Test coverage, test quality, edge cases +- **Performance**: Bundle size impact, rendering optimizations +- **Security**: Input validation, XSS prevention, credential handling +- **Accessibility**: ARIA labels, keyboard navigation, semantic HTML +- **Best Practices**: React patterns, Redux usage, NestJS conventions + +### 3. Requirements Check + +- Verify all acceptance criteria are met +- Check for missing functionality +- Validate edge case handling +- Ensure proper error messages + +### 4. Testing Validation + +- Unit test coverage (80% minimum) +- Integration tests for API endpoints +- Component tests for React components +- E2E tests for critical flows +- No fixed timeouts or magic numbers +- Use of faker for test data + +### 5. Generate Report + +Create a markdown report in `docs/reviews/pr--.md` with: + +**Note**: Use appropriate ticket reference format: + +- JIRA tickets: `pr-RI-1234-2024-11-20.md` +- GitHub issues: `pr-456-2024-11-20.md` + +Report should include: + +- **Summary**: Overview of changes +- **Strengths**: What was done well +- **Issues**: Categorized by severity (Critical, High, Medium, Low) +- **Suggestions**: Improvements and optimizations +- **Requirements Coverage**: Acceptance criteria checklist +- **Testing Assessment**: Coverage and quality analysis +- **Risk Assessment**: Potential issues or impacts + +## Review Checklist + +### Code Quality + +- [ ] No linting errors +- [ ] TypeScript types are proper (no `any` without justification) +- [ ] Code follows project conventions +- [ ] No console.log statements +- [ ] Import order is correct +- [ ] Cognitive complexity within limits +- [ ] No duplicate code + +### Testing + +- [ ] Unit tests added/updated +- [ ] Test coverage meets thresholds +- [ ] Tests use faker for data generation +- [ ] No fixed timeouts in tests +- [ ] Edge cases are tested +- [ ] Mocks are properly configured + +### React/Frontend (if applicable) + +- [ ] Functional components with hooks +- [ ] Proper state management (Redux vs local) +- [ ] Effects cleanup properly +- [ ] No unnecessary re-renders +- [ ] Accessibility considerations +- [ ] Styled-components for styling (no new SCSS modules) +- [ ] Proper error boundaries +- [ ] Component folder structure follows guidelines + +### NestJS/Backend (if applicable) + +- [ ] Dependency injection used properly +- [ ] DTOs for validation +- [ ] Proper error handling +- [ ] Swagger documentation +- [ ] Service layer separation +- [ ] Database transactions where needed + +### Performance + +- [ ] No performance regressions +- [ ] Large lists virtualized +- [ ] Routes lazy loaded +- [ ] Expensive operations memoized +- [ ] Bundle size impact acceptable + +### Security + +- [ ] Input validation +- [ ] Output sanitization +- [ ] No sensitive data in logs +- [ ] Proper authentication/authorization +- [ ] SQL injection prevention (if applicable) + +### Documentation + +- [ ] README updated if needed +- [ ] Complex logic documented +- [ ] API documentation updated +- [ ] Breaking changes noted + +## Example Output + +**Note**: Use appropriate ticket format (RI-1234 for JIRA or #456 for GitHub issues) + +```markdown +# PR Review: RI-1234 - Add User Profile Editing + +**Date**: 2024-11-20 +**Reviewer**: AI Assistant +**Branch**: feature/RI-1234/user-profile-editing + +## Summary + +This PR implements user profile editing functionality including UI components, +API endpoints, and data persistence. The implementation follows project +standards with good test coverage. + +## Strengths + +- ✅ Comprehensive test coverage (92%) +- ✅ Proper TypeScript typing throughout +- ✅ Good separation of concerns +- ✅ Follows Redux patterns correctly +- ✅ Proper error handling + +## Critical Issues + +None found. + +## High Priority Issues + +1. **Missing Input Validation** (Security) + - File: `redisinsight/api/src/user/user.service.ts:45` + - Issue: Email validation missing on backend + - Recommendation: Add class-validator decorator to DTO + +## Medium Priority Issues + +1. **Performance Concern** (Performance) + + - File: `redisinsight/ui/src/components/UserProfile.tsx:78` + - Issue: Inline function in render + - Recommendation: Extract to useCallback + +2. **Test Flakiness Risk** (Testing) + - File: `redisinsight/ui/src/components/UserProfile.spec.tsx:45` + - Issue: Direct state check without waitFor + - Recommendation: Wrap assertion in waitFor + +## Low Priority Issues + +1. **Code Style** (Style) + - File: Multiple files + - Issue: Inconsistent import ordering + - Recommendation: Run `yarn prettier:fix` + +## Suggestions + +- Consider adding optimistic updates for better UX +- Extract form validation logic to reusable hook +- Add E2E test for complete profile edit flow + +## Requirements Coverage + +- [x] User can edit profile name +- [x] User can edit profile email +- [x] Changes are persisted to database +- [x] Validation errors are displayed +- [ ] Email verification sent on email change (Missing) +- [x] Success message shown on save + +## Testing Assessment + +- **Unit Test Coverage**: 92% (Excellent) +- **Integration Tests**: 3 tests covering all endpoints (Good) +- **Component Tests**: 8 tests covering main scenarios (Good) +- **E2E Tests**: Not included (Consider adding) + +## Risk Assessment + +**Low Risk** - Well-tested implementation with minor issues that can be +addressed before merge. No breaking changes or security vulnerabilities. + +## Recommendation + +**Approve with comments** - Address high priority issues before merging. +Consider suggestions for future improvements. +``` + +## Notes + +- Focus on constructive feedback +- Prioritize issues by severity +- Be specific with file locations and line numbers +- Provide actionable recommendations +- Balance criticism with recognition of good practices +- Consider the broader impact of changes diff --git a/.ai/commands/run-ui-tests.md b/.ai/commands/run-ui-tests.md new file mode 100644 index 0000000000..3264e8f546 --- /dev/null +++ b/.ai/commands/run-ui-tests.md @@ -0,0 +1,167 @@ +# Running UI Tests + +## Custom Test Runner + +RedisInsight has a custom test runner script for running frontend tests with additional options. + +## Usage + +### Run Specific Test File + +```bash +cd ~/Projects/RedisInsight +./vendor/scripts/run_tests.sh -f +``` + +Example: + +```bash +./vendor/scripts/run_tests.sh -f redisinsight/ui/src/components/UserProfile/UserProfile.spec.tsx +``` + +### Run All Tests + +```bash +cd ~/Projects/RedisInsight +./vendor/scripts/run_tests.sh -w 4 +``` + +## Options + +The test runner (`run_tests.sh`) provides several options: + +- `-d, --directory DIR`: Working directory (default: current directory) +- `-n, --iterations NUM`: Number of test iterations to run (default: 4) +- `-c, --coverage`: Enable coverage reporting +- `-w, --workers NUM`: Number of workers for Jest (default: 1) +- `-f, --file PATH`: Path to specific test file to run +- `--ui`: Run UI tests (default) +- `--api`: Run API tests + +## Environment Issues + +If you encounter environment issues, source your shell configuration first: + +```bash +source ~/.zshrc +cd ~/Projects/RedisInsight +./vendor/scripts/run_tests.sh -f +``` + +## Standard Test Commands + +For standard test running (without the custom script): + +```bash +# Run all UI tests +yarn test + +# Run specific test +yarn test -- --testNamePattern="test name" + +# Run with coverage +yarn test:cov + +# Run in watch mode +yarn test:watch +``` + +## Running API Tests + +```bash +# Run all API tests +yarn test:api + +# Run API integration tests +yarn test:api:integration +``` + +## Test File Location + +- Test files are co-located with source files +- Use `.spec.ts` or `.spec.tsx` extension +- Example: `Component.tsx` → `Component.spec.tsx` + +## Notes + +- The custom runner path (`./vendor/scripts/run_tests.sh`) may be in `.gitignore` +- The `-w 4` flag runs tests with 4 workers for faster execution +- Coverage reports are generated in `/report/coverage/` +- The custom runner supports both UI and API tests + +## Common Issues + +### Path Not Found + +If the test runner script is not found: + +```bash +# Check if the file exists +ls -la ./vendor/scripts/run_tests.sh + +# Make sure you're in the project root +pwd +``` + +### Environment Variables + +Some tests may require specific environment variables: + +```bash +export NODE_ENV=test +export RI_APP_TYPE=web +``` + +### Jest Cache Issues + +If tests are failing due to cache: + +```bash +yarn test --clearCache +``` + +## Examples + +### Run Single Component Test + +```bash +./vendor/scripts/run_tests.sh -f redisinsight/ui/src/components/Header/Header.spec.tsx +``` + +### Run Tests with Coverage + +```bash +./vendor/scripts/run_tests.sh -f redisinsight/ui/src/slices/user/user.spec.ts -c +``` + +### Run Tests Multiple Times + +```bash +./vendor/scripts/run_tests.sh -f redisinsight/ui/src/utils/validation.spec.ts -n 10 +``` + +### Run All Tests with Multiple Workers + +```bash +./vendor/scripts/run_tests.sh -w 4 -c +``` + +## Integration with AI Assistants + +When asked to "run the tests" or "run UI tests": + +1. Use the custom test runner if available +2. Provide the full path from repository root +3. Use appropriate worker count for all tests (`-w 4`) +4. Remember to `cd` to project directory first + +## Quick Reference + +| Command | Description | +| ----------------------------------------- | ---------------------------- | +| `./vendor/scripts/run_tests.sh -f ` | Run specific test file | +| `./vendor/scripts/run_tests.sh -w 4` | Run all tests with 4 workers | +| `./vendor/scripts/run_tests.sh -c` | Run with coverage | +| `./vendor/scripts/run_tests.sh -n 10` | Run 10 iterations | +| `yarn test` | Standard Jest runner | +| `yarn test:cov` | Standard with coverage | diff --git a/.ai/rules/01-CODE_QUALITY.md b/.ai/rules/01-CODE_QUALITY.md new file mode 100644 index 0000000000..06ae5767bd --- /dev/null +++ b/.ai/rules/01-CODE_QUALITY.md @@ -0,0 +1,354 @@ +# Code Quality Standards + +## Linting and Formatting + +### Critical Rules + +- **ALWAYS run linter after code changes**: `yarn lint` or `yarn lint:ui` / `yarn lint:api` +- Linter must pass before committing +- Follow ESLint Airbnb config with project-specific overrides +- Use Prettier for code formatting +- No console.log in production code (use console.warn/error only) + +### ESLint Configuration + +- **UI**: Airbnb TypeScript + React + SonarJS +- **API**: Airbnb TypeScript Base + SonarJS +- **Cognitive Complexity Limits**: + - Backend/API: ≤ 15 + - Frontend/UI: ≤ 20 +- **Max Line Length**: 120 characters + +### Prettier Rules + +```javascript +{ + "semi": true, // Semicolons for .js files + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx"], + "options": { + "semi": false // NO semicolons for TypeScript + } + } + ] +} +``` + +### Code Style Examples + +```typescript +// ✅ GOOD: No semicolons in TypeScript +const value = 'test'; +const result = doSomething(); + +// ✅ GOOD: Proper typing +interface UserProfile { + id: string; + name: string; + email: string; +} + +const fetchUser = async (id: string): Promise => { + // Implementation +}; + +// ❌ BAD: Semicolons in TypeScript +const value = 'test'; + +// ❌ BAD: Using any without justification +const data: any = {}; + +// ❌ BAD: Magic numbers +const result = items.filter((x) => x.v > 100); + +// ✅ GOOD: Named constants +const MIN_THRESHOLD = 100; +const result = items.filter((item) => item.value > MIN_THRESHOLD); +``` + +## TypeScript Standards + +### Type Usage + +- Use TypeScript for **all new code** (no .js files unless required) +- Use **explicit return types** for functions when non-obvious +- **Prefer interfaces** over types for object shapes +- Use proper typing, **avoid `any`** unless absolutely necessary +- Leverage **type inference** where it improves readability +- Use **generics** for reusable components/functions + +### Interface vs Type + +```typescript +// ✅ PREFERRED: Interface for object shapes +interface User { + id: string; + name: string; + email: string; +} + +// ✅ OK: Type for unions, intersections, primitives +type Status = 'active' | 'inactive' | 'pending'; +type ID = string | number; + +// ✅ GOOD: Generic types +interface ApiResponse { + data: T; + error: string | null; + loading: boolean; +} +``` + +### Type Safety + +```typescript +// ✅ GOOD: Proper typing +const parseData = (input: string): ParsedData => { + const result = JSON.parse(input); + return result as ParsedData; +}; + +// ✅ GOOD: Type guards +function isUser(obj: unknown): obj is User { + return typeof obj === 'object' && obj !== null && 'id' in obj; +} + +// ❌ BAD: Too many any types +const processData = (data: any): any => { + return data.map((item: any) => item.value); +}; +``` + +## Import Organization + +### Import Order (Enforced by ESLint) + +1. **External libraries** (e.g., `react`, `lodash`) +2. **Built-in Node modules** (e.g., `path`, `fs`) +3. **Internal modules** (using aliases) +4. **Sibling/parent imports** +5. **Index imports** +6. **Style imports** (`.scss`) - **ALWAYS LAST** + +### Example + +```typescript +// 1. External libraries +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { some, debounce } from 'lodash'; + +// 2. Built-in Node modules (backend only) +import * as path from 'path'; +import * as fs from 'fs'; + +// 3. Internal modules (use aliases) +import { fetchData } from 'uiSrc/services/api'; +import { formatDate } from 'uiSrc/utils/formatters'; + +// 4. Sibling/parent imports +import { Component } from './Component'; +import { helper } from '../utils/helper'; + +// 5. Styles - ALWAYS LAST +import styles from './Component.module.scss'; +``` + +### Module Aliases + +Always use module aliases for cross-module imports: + +- `uiSrc/*` → `redisinsight/ui/src/*` +- `apiSrc/*` → `redisinsight/api/src/*` +- `desktopSrc/*` → `redisinsight/desktop/src/*` + +```typescript +// ✅ GOOD: Use aliases +import { UserService } from 'apiSrc/services/user'; +import { Button } from 'uiSrc/components/Button'; + +// ❌ BAD: Relative paths across modules +import { UserService } from '../../../api/src/services/user'; +``` + +## SonarJS Rules + +### Complexity Rules + +- **Cognitive Complexity**: Keep functions simple and readable + - Backend: ≤ 15 + - Frontend: ≤ 20 +- **No Duplicate Strings**: Extract repeated strings to constants +- **No Identical Functions**: Follow DRY principle +- **Prefer Immediate Return**: Avoid unnecessary intermediate variables + +### Examples + +```typescript +// ✅ GOOD: Low complexity +const calculateTotal = (items: Item[]): number => { + return items.reduce((sum, item) => sum + item.price, 0); +}; + +// ✅ GOOD: Extract constants +const ERROR_MESSAGES = { + NOT_FOUND: 'Entity not found', + INVALID_INPUT: 'Invalid input provided', + UNAUTHORIZED: 'Unauthorized access', +}; + +throw new NotFoundException(ERROR_MESSAGES.NOT_FOUND); + +// ✅ GOOD: Immediate return +const isValid = (value: string): boolean => { + return value.length > 0 && value.length < 100; +}; + +// ❌ BAD: Unnecessary variable +const isValid = (value: string): boolean => { + const result = value.length > 0 && value.length < 100; + return result; +}; +``` + +## Code Organization + +### File Structure + +```typescript +// 1. Imports (in proper order) +import React from 'react'; +import { useSelector } from 'react-redux'; +import { formatDate } from 'uiSrc/utils'; +import styles from './Component.module.scss'; + +// 2. Types/Interfaces +interface Props { + id: string; + name: string; +} + +interface State { + loading: boolean; +} + +// 3. Constants +const MAX_ITEMS = 100; +const DEFAULT_TIMEOUT = 5000; + +// 4. Component/Function implementation +export const Component: FC = ({ id, name }) => { + // Implementation +}; + +// 5. Helper functions (if small and local) +const formatName = (name: string): string => { + return name.trim().toLowerCase(); +}; +``` + +### Naming Conventions + +```typescript +// ✅ GOOD: Descriptive names +const fetchUserProfile = async (userId: string) => {}; +const isValidEmail = (email: string): boolean => {}; +const MAX_RETRY_ATTEMPTS = 3; + +// ❌ BAD: Unclear names +const getData = async (id: string) => {}; +const check = (val: string): boolean => {}; +const MAX = 3; + +// ✅ GOOD: Component names (PascalCase) +export const UserProfile: FC = () => {}; + +// ✅ GOOD: Boolean variables (is/has/should prefix) +const isLoading = true; +const hasError = false; +const shouldRetry = true; +``` + +## Best Practices + +### Destructuring + +```typescript +// ✅ GOOD: Use destructuring +const { name, email, age } = user; +const [first, second, ...rest] = items; + +// ❌ BAD: Repetitive access +const name = user.name; +const email = user.email; +const age = user.age; +``` + +### Template Literals + +```typescript +// ✅ GOOD: Template literals +const message = `Hello ${name}, you have ${count} messages`; + +// ❌ BAD: String concatenation +const message = 'Hello ' + name + ', you have ' + count + ' messages'; +``` + +### Const vs Let + +```typescript +// ✅ GOOD: Use const by default +const userId = '123'; +const items = [1, 2, 3]; + +// ✅ GOOD: Use let only when reassignment needed +let counter = 0; +counter += 1; + +// ❌ BAD: Unnecessary let +let name = 'John'; // Never reassigned +``` + +### No Console.log + +```typescript +// ❌ BAD: console.log in production +console.log('User data:', userData); + +// ✅ GOOD: Use proper logging (backend) +this.logger.debug('User data', { userId: user.id }); +this.logger.error('Failed to fetch user', error); + +// ✅ GOOD: console.error/warn for errors (frontend) +console.error('API Error:', error); +console.warn('Deprecated method used'); +``` + +## Common Pitfalls to Avoid + +1. ❌ Using `any` type unnecessarily +2. ❌ Not cleaning up effects and subscriptions +3. ❌ Magic numbers and unclear variable names +4. ❌ Duplicate code (DRY violation) +5. ❌ High cognitive complexity +6. ❌ Console.log statements +7. ❌ Ignoring linter errors +8. ❌ Mutating objects directly (Redux) +9. ❌ Missing error handling +10. ❌ Unnecessary comments + +## Verification Checklist + +Before committing: + +- [ ] `yarn lint` passes with no errors +- [ ] No TypeScript errors +- [ ] Import order is correct +- [ ] No `any` types without justification +- [ ] No console.log statements +- [ ] No magic numbers +- [ ] Variable names are descriptive +- [ ] Functions have low cognitive complexity +- [ ] No duplicate code +- [ ] Code is properly formatted diff --git a/.ai/rules/02-FRONTEND.md b/.ai/rules/02-FRONTEND.md new file mode 100644 index 0000000000..a4a7282c2b --- /dev/null +++ b/.ai/rules/02-FRONTEND.md @@ -0,0 +1,852 @@ +# Frontend Development (React/Redux) + +## Table of Contents + +1. [Component Structure & Organization](#component-structure--organization) +2. [Styled Components](#styled-components) +3. [State Management (Redux)](#state-management-redux) +4. [React Best Practices](#react-best-practices) +5. [Custom Hooks](#custom-hooks) +6. [Form Handling](#form-handling) +7. [UI Components](#ui-components) +8. [Common Pitfalls](#common-pitfalls) + +--- + +## Component Structure & Organization + +### Functional Components + +- Use **functional components with hooks** (no class components) +- **Prefer named exports** over default exports +- Keep components **focused and single-responsibility** +- Extract complex logic into **custom hooks** or utilities + +### Component Folder Structure + +**Note**: We are **migrating to styled-components** and deprecating SCSS modules. + +Each component should be placed in its own directory under `**/ComponentName`. The directory should contain: + +- `ComponentName.tsx` – The main component file +- `ComponentName.style.ts` – Styles using styled-components +- `ComponentName.types.ts` – TypeScript types and interfaces +- `ComponentName.test.tsx` – Test of the component +- `ComponentName.constants.ts` – All of the relevant constants of the component +- `ComponentName.story.tsx` - Storybook with examples how to use this component +- `/components` - Any sub components used within the main component +- `/hooks` - Any custom hooks used within the main component +- `/utils` - Any utility functions used within the main component + +#### Example Structure + +``` +UserDetails/ + UserDetails.tsx + UserDetails.style.ts + UserDetails.types.ts + UserDetails.test.tsx + UserDetails.constants.ts + hooks/ + useUserDetailsLogic.ts + components/ + UserAddress/ + hooks/ + useUserAddressLogic.ts + UserAddress.tsx + UserAddress.style.ts + UserAddress.types.ts + utils/ + getUserFullName/ + getUserFullName.ts + getUserFullName.test.ts +``` + +### Component Pattern + +```typescript +// UserProfile.types.ts +export interface UserProfileProps { + userId: string + onUpdate?: (user: User) => void +} + +export interface User { + id: string + name: string + email: string +} + +// UserProfile.constants.ts +export const ERROR_MESSAGES = { + FETCH_ERROR: 'Failed to load user profile', + UPDATE_ERROR: 'Failed to update user profile', +} + +export const REFRESH_INTERVAL = 5000 + +// UserProfile.style.ts +import styled from 'styled-components/macro' + +export const Container = styled.div` + padding: 16px; + background: white; + border-radius: 8px; +` + +export const Title = styled.h2` + font-size: 24px; + font-weight: bold; + margin-bottom: 16px; +` + +// hooks/useUserProfile.ts +import { useState, useEffect } from 'react' +import { User } from '../UserProfile.types' +import { fetchUser } from 'uiSrc/services/api' + +export const useUserProfile = (userId: string) => { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + setLoading(true) + fetchUser(userId) + .then(setUser) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)) + }, [userId]) + + return { user, loading, error } +} + +// UserProfile.tsx +import React, { FC } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { fetchUserData } from 'uiSrc/slices/user/thunks' +import { userSelector, loadingSelector } from 'uiSrc/slices/user/selectors' + +import { UserProfileProps } from './UserProfile.types' +import { ERROR_MESSAGES } from './UserProfile.constants' +import { useUserProfile } from './hooks/useUserProfile' + +import * as S from './UserProfile.style' + +export const UserProfile: FC = ({ userId, onUpdate }) => { + const dispatch = useDispatch() + const { user, loading, error } = useUserProfile(userId) + + useEffect(() => { + dispatch(fetchUserData(userId)) + + // Cleanup + return () => { + // Cancel subscriptions, clear timers, etc. + } + }, [userId, dispatch]) + + const handleSave = useCallback(() => { + // Handler logic + onUpdate?.(user) + }, [user, onUpdate]) + + if (loading) return
Loading...
+ if (error) return
{ERROR_MESSAGES.FETCH_ERROR}
+ if (!user) return null + + return ( + + {user.name} +

{user.email}

+
+ ) +} +``` + +### Props Interface Naming + +- Name component props interface as `ComponentNameProps` +- Use separate interfaces for complex prop objects +- Always use proper TypeScript types, never `any` + +#### ✅ Good + +```tsx +// ComponentName.types.ts +export interface ComponentNameProps { + required: string; + optional?: number; + callback: (id: string) => void; +} + +export interface UserData { + id: string; + name: string; + email: string; +} +``` + +### Use Clear and Descriptive Prop Names + +#### ✅ Good + +```tsx + +``` + +#### ❌ Bad + +```tsx + +``` + +### Imports Order in Components + +```tsx +// 1. External dependencies +import React, { FC, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +// 2. Internal modules (aliases) +import { fetchUser } from 'uiSrc/services/api'; +import { userSelector } from 'uiSrc/slices/user/selectors'; + +// 3. Local imports +import { UserProfileProps } from './UserProfile.types'; +import { ERROR_MESSAGES } from './UserProfile.constants'; +import { useUserProfile } from './hooks/useUserProfile'; + +// 4. Styles (always last) +import * as S from './UserProfile.style'; +``` + +### Barrel Files + +Avoid short barrel files. Use barrel files (`index.ts`) only when exporting **3 or more** items: + +#### ✅ Good (3+ exports) + +```tsx +// components/index.ts +export { UserDetails } from './UserDetails/UserDetails'; +export { UserAddress } from './UserAddress/UserAddress'; +export { UserProfile } from './UserProfile/UserProfile'; +``` + +#### ❌ Bad (less than 3 exports) + +```tsx +// components/index.ts +export { UserDetails } from './UserDetails/UserDetails'; +export { UserAddress } from './UserAddress/UserAddress'; +``` + +**Important**: Make sure an export happens only in a single barrel file, and is not propagated up the chain of barrel files. + +--- + +## Styled Components + +### Encapsulating Styles in .style.ts File + +Keep all component styles in a dedicated `.style.ts` file using styled-components. This improves maintainability and consistency. + +#### ✅ Good + +```tsx +// HeaderBar.style.ts +import styled from 'styled-components/macro'; + +export const Container = styled.div` + display: flex; + padding: 16px; + background-color: ${({ theme }) => theme.colors.background}; +`; + +export const Title = styled.div` + color: red; + font-weight: bold; + font-size: 24px; +`; + +export const Subtitle = styled.span` + color: gray; + font-size: 14px; +`; + +// HeaderBar.tsx +import * as S from './HeaderBar.style'; + +return ( + + {title} + {subtitle} + +); +``` + +#### ❌ Bad + +```tsx +// HeaderBar.tsx - inline styles +return
{title}
; +``` + +### Import Pattern + +Use the pattern `import * as S from './Component.style'` to namespace all styled components: + +```tsx +import * as S from './Component.style'; + +return ( + + Title + Content + +); +``` + +This makes it immediately clear which elements are styled components. + +### Conditional Styling + +```tsx +// Component.style.ts +import styled from 'styled-components/macro' + +export const Button = styled.button<{ $isActive?: boolean }>` + padding: 8px 16px; + background-color: ${({ $isActive }) => ($isActive ? '#007bff' : '#6c757d')}; + color: white; + border: none; + border-radius: 4px; + + &:hover { + opacity: 0.9; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +` + +// Component.tsx +Click Me +``` + +**Note**: Use `$` prefix for transient props that shouldn't be passed to the DOM. + +--- + +## State Management (Redux) + +### When to Use What + +- **Global State (Redux)**: + + - Data shared across multiple components + - Data that persists across routes + - Server state (API data) + - User preferences/settings + +- **Local State (useState)**: + + - UI state (modals, dropdowns, tabs) + - Form inputs before submission + - Component-specific temporary data + +- **Derived State (Selectors)**: + - Computed values from Redux state + - Filtered/sorted lists + - Aggregated data + +### Redux Toolkit Patterns + +#### Slice Structure + +```typescript +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface UserState { + users: User[]; + selectedId: string | null; + loading: boolean; + error: string | null; +} + +const initialState: UserState = { + users: [], + selectedId: null, + loading: false, + error: null, +}; + +export const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + setUsers: (state, { payload }: PayloadAction) => { + state.users = payload; + }, + selectUser: (state, { payload }: PayloadAction) => { + state.selectedId = payload; + }, + setLoading: (state, { payload }: PayloadAction) => { + state.loading = payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchUsers.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchUsers.fulfilled, (state, action) => { + state.loading = false; + state.users = action.payload; + }) + .addCase(fetchUsers.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }); + }, +}); + +export const { setUsers, selectUser, setLoading } = userSlice.actions; +export default userSlice.reducer; +``` + +#### Thunks for Async Actions + +```typescript +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { apiService } from 'uiSrc/services/api'; + +export const fetchUsers = createAsyncThunk( + 'user/fetchUsers', + async (_, { rejectWithValue }) => { + try { + const response = await apiService.get('/users'); + return response.data; + } catch (error) { + return rejectWithValue(error.message); + } + }, +); + +export const updateUser = createAsyncThunk( + 'user/updateUser', + async ( + { id, data }: { id: string; data: UserUpdate }, + { rejectWithValue }, + ) => { + try { + const response = await apiService.put(`/users/${id}`, data); + return response.data; + } catch (error) { + return rejectWithValue(error.message); + } + }, +); +``` + +#### Selectors + +```typescript +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'uiSrc/slices/store'; + +// Basic selectors +export const usersSelector = (state: RootState) => state.user.users; +export const selectedIdSelector = (state: RootState) => state.user.selectedId; +export const loadingSelector = (state: RootState) => state.user.loading; + +// Memoized selector (reselect) +export const selectedUserSelector = createSelector( + [usersSelector, selectedIdSelector], + (users, selectedId) => users.find((user) => user.id === selectedId), +); + +// Complex derived state +export const activeUsersSelector = createSelector([usersSelector], (users) => + users.filter((user) => user.status === 'active'), +); +``` + +#### Redux in Components + +```tsx +import { useSelector, useDispatch } from 'react-redux'; +import { userActions } from 'uiSrc/slices/user'; +import { userSelector } from 'uiSrc/slices/user/selectors'; + +export const UserProfile: FC = () => { + const dispatch = useDispatch(); + const user = useSelector(userSelector); + + const handleUpdate = () => { + dispatch(userActions.updateUser({ ...user })); + }; + + return {/* ... */}; +}; +``` + +--- + +## React Best Practices + +### Performance Optimization + +```typescript +// ✅ GOOD: useCallback for functions passed as props +const handleClick = useCallback(() => { + dispatch(selectUser(userId)) +}, [userId, dispatch]) + +// ✅ GOOD: useMemo for expensive computations +const sortedUsers = useMemo(() => { + return users.sort((a, b) => a.name.localeCompare(b.name)) +}, [users]) + +// ✅ GOOD: React.memo for expensive components +export const UserCard = React.memo(({ user }) => { + return
{user.name}
+}, (prevProps, nextProps) => { + // Custom comparison + return prevProps.user.id === nextProps.user.id +}) + +// ❌ BAD: Inline functions in JSX (creates new function on every render) + + +// ✅ GOOD: Extract to useCallback + +``` + +### Effect Cleanup + +```typescript +// ✅ GOOD: Proper cleanup +useEffect(() => { + const subscription = api.subscribe(handleUpdate); + const timer = setTimeout(() => {}, 1000); + const listener = window.addEventListener('resize', handleResize); + + return () => { + subscription.unsubscribe(); + clearTimeout(timer); + window.removeEventListener('resize', handleResize); + }; +}, []); + +// ❌ BAD: No cleanup +useEffect(() => { + const interval = setInterval(() => { + fetchData(); + }, 1000); + // Memory leak! Interval never cleared +}, []); +``` + +### Keys in Lists + +```typescript +// ✅ GOOD: Use unique, stable IDs +users.map(user => ( + +)) + +// ❌ BAD: Using array indices +users.map((user, index) => ( + +)) + +// ⚠️ ACCEPTABLE: Only if list never reorders and items have no IDs +staticItems.map((item, index) => ( + +)) +``` + +### Conditional Rendering + +```typescript +// ✅ GOOD: Early returns +if (loading) { + return +} + +if (error) { + return +} + +return + +// ✅ GOOD: Conditional content +{isEditing && } +{user ? : } + +// ❌ BAD: Nested ternaries (hard to read) +{user ? ( + user.isAdmin ? ( + + ) : ( + + ) +) : ( + +)} + +// ✅ BETTER: Extract to function or use early returns +const renderPanel = () => { + if (!user) return + if (user.isAdmin) return + return +} + +return
{renderPanel()}
+``` + +--- + +## Custom Hooks + +### Extract Reusable Logic + +```typescript +// useDebounce.ts +import { useState, useEffect } from 'react' + +export const useDebounce = (value: T, delay: number): T => { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(handler) + } + }, [value, delay]) + + return debouncedValue +} + +// Usage +const SearchComponent: FC = () => { + const [searchTerm, setSearchTerm] = useState('') + const debouncedSearchTerm = useDebounce(searchTerm, 500) + + useEffect(() => { + if (debouncedSearchTerm) { + dispatch(searchUsers(debouncedSearchTerm)) + } + }, [debouncedSearchTerm]) + + return setSearchTerm(e.target.value)} /> +} +``` + +### Component-Specific Hooks + +Store component-specific hooks in the component's `/hooks` directory: + +```tsx +// hooks/useUserDetailsLogic.ts +import { useState, useEffect } from 'react'; + +export const useUserDetailsLogic = (userId: string) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchUser(userId) + .then(setUser) + .finally(() => setLoading(false)); + }, [userId]); + + return { user, loading }; +}; + +// UserDetails.tsx +import { useUserDetailsLogic } from './hooks/useUserDetailsLogic'; + +const UserDetails = ({ userId }) => { + const { user, loading } = useUserDetailsLogic(userId); + // ... render logic +}; +``` + +--- + +## Form Handling + +### Formik Integration + +```typescript +import { Formik, Form, Field } from 'formik' +import * as Yup from 'yup' + +const validationSchema = Yup.object({ + name: Yup.string().required('Name is required'), + email: Yup.string().email('Invalid email').required('Email is required'), +}) + +export const UserForm: FC = () => { + const handleSubmit = async (values: FormValues) => { + try { + await dispatch(updateUser(values)) + } catch (error) { + console.error('Failed to update user', error) + } + } + + return ( + + {({ errors, touched }) => ( +
+ + {errors.name && touched.name &&
{errors.name}
} + + + {errors.email && touched.email &&
{errors.email}
} + + + + )} +
+ ) +} +``` + +--- + +## UI Components + +**⚠️ IMPORTANT**: We are **deprecating Elastic UI** components and migrating to **Redis UI** (`@redis-ui/*`) everywhere. + +**📦 Component Architecture**: We use **internal wrappers** around Redis UI components. Do not import from `@redis-ui/*` directly. Instead, import from our internal component library which re-exports wrapped components. + +### Internal Component Wrappers (Preferred) + +```typescript +// ✅ GOOD: Import from internal wrappers +import { Button, Input, FlexGroup, FlexItem } from 'uiSrc/components/ui' + +// Our wrappers re-export Redis UI components with project-specific defaults and styling +export const Form: FC = () => { + return ( + + + setName(e.target.value)} + /> + + + + + + ) +} +``` + +```typescript +// ❌ BAD: Don't import directly from @redis-ui +import { Button } from '@redis-ui/components'; + +// ✅ GOOD: Import from internal wrappers +import { Button } from 'uiSrc/components/ui'; +``` + +### Creating Internal Wrappers + +When wrapping a new Redis UI component: + +```typescript +// uiSrc/components/ui/Button/Button.tsx +import { Button as RedisButton } from '@redis-ui/components' +import { ButtonProps } from './Button.types' + +export const Button: FC = (props) => { + // Add project-specific logic, defaults, or styling here + return +} + +// uiSrc/components/ui/index.ts +export { Button } from './Button/Button' +export { Input } from './Input/Input' +// ... other wrapped components +``` + +### Elastic UI Components (Deprecated) + +```typescript +// ❌ DEPRECATED: Don't use Elastic UI for new components +import { EuiButton, EuiFieldText } from '@elastic/eui'; + +// Only acceptable in existing components not yet migrated +// All new components should use internal wrappers instead +``` + +**Migration Guidelines**: + +- ✅ Use internal wrappers from `uiSrc/components/ui` for all new features +- ✅ Create internal wrappers for Redis UI components as needed +- ✅ Replace Elastic UI components when touching existing code +- ❌ Do not import directly from `@redis-ui/*` +- ❌ Do not add new Elastic UI component imports +- 📝 Check `uiSrc/components/ui` for available wrapped components first + +--- + +## Common Pitfalls + +1. ❌ Inline arrow functions in JSX props +2. ❌ Not cleaning up effects +3. ❌ Using array indices as keys +4. ❌ Mutating Redux state directly +5. ❌ Not memoizing expensive computations +6. ❌ Missing dependency arrays in useEffect +7. ❌ Overusing global state +8. ❌ Not handling loading/error states +9. ❌ Large components doing too much +10. ❌ Direct DOM manipulation (use refs sparingly) +11. ❌ Using inline styles instead of styled-components +12. ❌ Not separating types into `.types.ts` files +13. ❌ Creating barrel files with less than 3 exports +14. ❌ Using Elastic UI components in new code (deprecated) +15. ❌ Importing directly from `@redis-ui/*` (use internal wrappers instead) + +## Performance Checklist + +- [ ] Expensive components wrapped in React.memo +- [ ] Functions passed as props wrapped in useCallback +- [ ] Expensive computations wrapped in useMemo +- [ ] Long lists virtualized (react-window/react-virtualized) +- [ ] Routes lazy loaded +- [ ] Large dependencies code-split +- [ ] Images optimized +- [ ] No unnecessary re-renders + +## Key Principles + +1. **Separation of Concerns**: Keep styles, types, constants, logic, and presentation separate +2. **Colocate Related Code**: Keep sub-components, hooks, and utilities close to where they're used +3. **Consistent Naming**: Follow naming conventions consistently across all components +4. **Type Safety**: Always define proper TypeScript types, never use `any` +5. **Testability**: Structure components to be easily testable +6. **Readability**: Organize code to be easily understood by new developers +7. **Styled Components**: Prefer styled-components over SCSS modules (migration in progress) diff --git a/.ai/rules/03-BACKEND.md b/.ai/rules/03-BACKEND.md new file mode 100644 index 0000000000..6e6f5920d5 --- /dev/null +++ b/.ai/rules/03-BACKEND.md @@ -0,0 +1,797 @@ +# Backend Development (NestJS/API) + +## Table of Contents +1. [Module Structure & Organization](#module-structure--organization) +2. [Service Layer](#service-layer) +3. [Controller Layer](#controller-layer) +4. [Data Transfer Objects (DTOs)](#data-transfer-objects-dtos) +5. [Error Handling](#error-handling) +6. [Redis Integration](#redis-integration) +7. [Database Operations](#database-operations) +8. [API Documentation (Swagger)](#api-documentation-swagger) +9. [Best Practices](#best-practices) +10. [Common Pitfalls](#common-pitfalls) + +--- + +## Module Structure & Organization + +### NestJS Architecture Principles +- Follow **modular architecture** (feature-based modules) +- Use **dependency injection** throughout +- **Separate concerns**: Controllers, Services, Repositories +- Use **DTOs** for validation and data transfer +- Apply **proper error handling** with NestJS exceptions + +### Module Folder Structure + +Each feature module should be organized in its own directory under `redisinsight/api/src/`. The directory should contain: + +``` +feature/ +├── feature.module.ts # Module definition +├── feature.controller.ts # REST endpoints +├── feature.service.ts # Business logic +├── feature.service.spec.ts # Service unit tests +├── feature.controller.spec.ts # Controller unit tests +├── dto/ # Data transfer objects +│ ├── create-feature.dto.ts +│ ├── update-feature.dto.ts +│ ├── feature.dto.ts +│ └── index.ts # Barrel file (3+ exports) +├── entities/ # TypeORM entities +│ ├── feature.entity.ts +│ └── index.ts +├── repositories/ # Custom repositories (if needed) +│ └── feature.repository.ts +├── exceptions/ # Custom exceptions +│ └── feature-not-found.exception.ts +├── guards/ # Feature-specific guards +│ └── feature-access.guard.ts +├── decorators/ # Custom decorators +│ └── feature-context.decorator.ts +└── constants/ # Feature constants + └── feature.constants.ts +``` + +#### Example Structure + +``` +user/ +├── user.module.ts +├── user.controller.ts +├── user.service.ts +├── user.service.spec.ts +├── user.controller.spec.ts +├── dto/ +│ ├── create-user.dto.ts +│ ├── update-user.dto.ts +│ ├── user.dto.ts +│ └── index.ts +├── entities/ +│ ├── user.entity.ts +│ ├── user-profile.entity.ts +│ └── index.ts +├── repositories/ +│ └── user.repository.ts +├── exceptions/ +│ ├── user-not-found.exception.ts +│ └── user-already-exists.exception.ts +├── guards/ +│ └── user-ownership.guard.ts +└── constants/ + └── user.constants.ts +``` + +### File Naming Conventions + +- **Module files**: `feature.module.ts` +- **Controller files**: `feature.controller.ts` +- **Service files**: `feature.service.ts` +- **DTO files**: `create-feature.dto.ts`, `update-feature.dto.ts`, `feature.dto.ts` +- **Entity files**: `feature.entity.ts` +- **Test files**: `feature.service.spec.ts`, `feature.controller.spec.ts` +- **Constants**: `feature.constants.ts` +- **Exceptions**: `feature-not-found.exception.ts` + +### Module Pattern +```typescript +// feature.module.ts +import { Module } from '@nestjs/common' +import { TypeOrmModule } from '@nestjs/typeorm' +import { FeatureController } from './feature.controller' +import { FeatureService } from './feature.service' +import { FeatureEntity } from './entities/feature.entity' + +@Module({ + imports: [TypeOrmModule.forFeature([FeatureEntity])], + controllers: [FeatureController], + providers: [FeatureService], + exports: [FeatureService], // Export if used by other modules +}) +export class FeatureModule {} +``` + +### Constants Organization + +Store feature-specific constants in a dedicated constants file: + +```typescript +// feature.constants.ts +export const FEATURE_CONSTANTS = { + MAX_NAME_LENGTH: 100, + MIN_NAME_LENGTH: 3, + DEFAULT_PAGE_SIZE: 20, + MAX_PAGE_SIZE: 100, +} as const + +export const FEATURE_ERROR_MESSAGES = { + NOT_FOUND: 'Feature not found', + ALREADY_EXISTS: 'Feature already exists', + INVALID_INPUT: 'Invalid feature data', + UNAUTHORIZED: 'Not authorized to access this feature', +} as const + +export const FEATURE_REDIS_KEYS = { + PREFIX: 'feature:', + LIST: 'feature:list', + DETAILS: (id: string) => `feature:${id}`, +} as const +``` + +### Barrel Files (index.ts) + +Use barrel files for exporting **3 or more** related items: + +#### ✅ Good (3+ exports) + +```typescript +// dto/index.ts +export { CreateFeatureDto } from './create-feature.dto' +export { UpdateFeatureDto } from './update-feature.dto' +export { FeatureDto } from './feature.dto' +export { FeatureResponseDto } from './feature-response.dto' +``` + +#### ❌ Bad (less than 3 exports) + +```typescript +// dto/index.ts +export { CreateFeatureDto } from './create-feature.dto' +export { UpdateFeatureDto } from './update-feature.dto' +``` + +### Imports Order + +```typescript +// 1. Node.js built-in modules +import { readFile } from 'fs/promises' + +// 2. External dependencies +import { Injectable, NotFoundException, Logger } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Repository } from 'typeorm' + +// 3. Internal modules (using aliases) +import { RedisClient } from 'apiSrc/modules/redis/redis.client' +import { ConfigService } from 'apiSrc/config/config.service' + +// 4. Local imports (relative) +import { CreateFeatureDto, UpdateFeatureDto } from './dto' +import { FeatureEntity } from './entities/feature.entity' +import { FEATURE_ERROR_MESSAGES } from './constants/feature.constants' +``` + +--- + +## Service Layer + +### Service Pattern +```typescript +import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Repository } from 'typeorm' +import { FeatureEntity } from './entities/feature.entity' +import { CreateFeatureDto, UpdateFeatureDto } from './dto' + +@Injectable() +export class FeatureService { + constructor( + @InjectRepository(FeatureEntity) + private readonly repository: Repository, + ) {} + + async findAll(): Promise { + try { + return await this.repository.find() + } catch (error) { + throw new InternalServerErrorException('Failed to fetch features') + } + } + + async findById(id: string): Promise { + const entity = await this.repository.findOne({ where: { id } }) + + if (!entity) { + throw new NotFoundException(`Feature with ID ${id} not found`) + } + + return entity + } + + async create(dto: CreateFeatureDto): Promise { + try { + const entity = this.repository.create(dto) + return await this.repository.save(entity) + } catch (error) { + throw new InternalServerErrorException('Failed to create feature') + } + } + + async update(id: string, dto: UpdateFeatureDto): Promise { + await this.findById(id) // Verify exists + + try { + await this.repository.update(id, dto) + return await this.findById(id) + } catch (error) { + throw new InternalServerErrorException('Failed to update feature') + } + } + + async delete(id: string): Promise { + const result = await this.repository.delete(id) + + if (result.affected === 0) { + throw new NotFoundException(`Feature with ID ${id} not found`) + } + } +} +``` + +### Dependency Injection +```typescript +// ✅ GOOD: Inject dependencies via constructor +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + private readonly emailService: EmailService, + private readonly logger: LoggerService, + ) {} +} + +// ❌ BAD: Direct instantiation +export class UserService { + private emailService = new EmailService() // Don't do this +} +``` + +## Controller Layer + +### Controller Pattern +```typescript +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + HttpCode, + HttpStatus, + UseGuards, +} from '@nestjs/common' +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, +} from '@nestjs/swagger' +import { FeatureService } from './feature.service' +import { CreateFeatureDto, UpdateFeatureDto, FeatureDto } from './dto' +import { AuthGuard } from '../auth/guards/auth.guard' + +@ApiTags('Features') +@Controller('api/features') +@UseGuards(AuthGuard) +@ApiBearerAuth() +export class FeatureController { + constructor(private readonly service: FeatureService) {} + + @Get() + @ApiOperation({ summary: 'Get all features' }) + @ApiResponse({ status: 200, type: [FeatureDto] }) + async findAll(): Promise { + return this.service.findAll() + } + + @Get(':id') + @ApiOperation({ summary: 'Get feature by ID' }) + @ApiResponse({ status: 200, type: FeatureDto }) + @ApiResponse({ status: 404, description: 'Feature not found' }) + async findById(@Param('id') id: string): Promise { + return this.service.findById(id) + } + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Create a new feature' }) + @ApiResponse({ status: 201, type: FeatureDto }) + @ApiResponse({ status: 400, description: 'Invalid input' }) + async create(@Body() dto: CreateFeatureDto): Promise { + return this.service.create(dto) + } + + @Put(':id') + @ApiOperation({ summary: 'Update feature' }) + @ApiResponse({ status: 200, type: FeatureDto }) + @ApiResponse({ status: 404, description: 'Feature not found' }) + async update( + @Param('id') id: string, + @Body() dto: UpdateFeatureDto, + ): Promise { + return this.service.update(id, dto) + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Delete feature' }) + @ApiResponse({ status: 204, description: 'Successfully deleted' }) + @ApiResponse({ status: 404, description: 'Feature not found' }) + async delete(@Param('id') id: string): Promise { + return this.service.delete(id) + } +} +``` + +## Data Transfer Objects (DTOs) + +### Validation with class-validator +```typescript +import { IsString, IsNotEmpty, IsOptional, IsEmail, MinLength, MaxLength } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class CreateFeatureDto { + @ApiProperty({ description: 'Feature name', example: 'New Feature' }) + @IsString() + @IsNotEmpty() + @MinLength(3) + @MaxLength(100) + name: string + + @ApiPropertyOptional({ description: 'Feature description' }) + @IsString() + @IsOptional() + @MaxLength(500) + description?: string + + @ApiProperty({ description: 'Feature value' }) + @IsString() + @IsNotEmpty() + value: string +} + +export class UpdateFeatureDto { + @ApiPropertyOptional() + @IsString() + @IsOptional() + @MinLength(3) + @MaxLength(100) + name?: string + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @MaxLength(500) + description?: string + + @ApiPropertyOptional() + @IsString() + @IsOptional() + value?: string +} + +export class FeatureDto { + @ApiProperty() + id: string + + @ApiProperty() + name: string + + @ApiProperty() + description: string + + @ApiProperty() + value: string + + @ApiProperty() + createdAt: Date + + @ApiProperty() + updatedAt: Date +} +``` + +## Error Handling + +### NestJS Exceptions +```typescript +import { + BadRequestException, + NotFoundException, + UnauthorizedException, + ForbiddenException, + InternalServerErrorException, + ConflictException, +} from '@nestjs/common' + +// ✅ GOOD: Use appropriate exception types +async findUser(id: string): Promise { + const user = await this.userRepository.findOne({ where: { id } }) + + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`) + } + + return user +} + +async createUser(dto: CreateUserDto): Promise { + const existing = await this.userRepository.findOne({ + where: { email: dto.email } + }) + + if (existing) { + throw new ConflictException('User with this email already exists') + } + + try { + return await this.userRepository.save(dto) + } catch (error) { + throw new InternalServerErrorException('Failed to create user') + } +} + +// ✅ GOOD: Custom exception filters +import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common' + +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const request = ctx.getRequest() + const status = exception.getStatus() + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message: exception.message, + }) + } +} +``` + +### Error Logging +```typescript +import { Logger } from '@nestjs/common' + +export class FeatureService { + private readonly logger = new Logger(FeatureService.name) + + async processData(data: Data): Promise { + try { + return await this.externalService.process(data) + } catch (error) { + this.logger.error( + `Failed to process data: ${error.message}`, + error.stack, + { data } + ) + throw new InternalServerErrorException('Processing failed') + } + } +} +``` + +## Redis Integration + +### Redis Service Pattern +```typescript +import { Injectable, Logger } from '@nestjs/common' +import { RedisClient } from 'apiSrc/modules/redis/redis.client' + +@Injectable() +export class RedisService { + private readonly logger = new Logger(RedisService.name) + + constructor(private readonly redisClient: RedisClient) {} + + async executeCommand(command: string, args: string[]): Promise { + try { + const client = await this.redisClient.getClient() + const result = await client.call(command, ...args) + return this.formatResult(result) + } catch (error) { + this.logger.error(`Redis command failed: ${command}`, error.stack) + throw new BadRequestException(`Redis error: ${error.message}`) + } + } + + async get(key: string): Promise { + try { + return await this.redisClient.get(key) + } catch (error) { + this.logger.error(`Failed to get key: ${key}`, error.stack) + throw new InternalServerErrorException('Redis operation failed') + } + } + + async set(key: string, value: string, ttl?: number): Promise { + try { + if (ttl) { + await this.redisClient.setex(key, ttl, value) + } else { + await this.redisClient.set(key, value) + } + } catch (error) { + this.logger.error(`Failed to set key: ${key}`, error.stack) + throw new InternalServerErrorException('Redis operation failed') + } + } +} +``` + +## Database Operations + +### Transactions +```typescript +import { DataSource } from 'typeorm' + +@Injectable() +export class UserService { + constructor( + private dataSource: DataSource, + @InjectRepository(User) + private userRepository: Repository, + ) {} + + async createUserWithProfile(userData: CreateUserDto): Promise { + const queryRunner = this.dataSource.createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction() + + try { + const user = await queryRunner.manager.save(User, userData) + await queryRunner.manager.save(Profile, { userId: user.id }) + + await queryRunner.commitTransaction() + return user + } catch (error) { + await queryRunner.rollbackTransaction() + throw new InternalServerErrorException('Transaction failed') + } finally { + await queryRunner.release() + } + } +} +``` + +## Code Quality Rules (SonarJS) + +### Cognitive Complexity (≤ 15 for API) +```typescript +// ✅ GOOD: Low complexity +async validateUser(email: string, password: string): Promise { + const user = await this.findByEmail(email) + + if (!user) { + throw new UnauthorizedException('Invalid credentials') + } + + const isValid = await this.comparePasswords(password, user.password) + + if (!isValid) { + throw new UnauthorizedException('Invalid credentials') + } + + return user +} + +// ❌ BAD: High complexity (nested conditions) +async processUser(user: User): Promise { + if (user) { + if (user.isActive) { + if (user.hasPermission) { + if (user.subscription) { + // Too deeply nested + } + } + } + } +} + +// ✅ GOOD: Refactored with early returns +async processUser(user: User): Promise { + if (!user) throw new NotFoundException('User not found') + if (!user.isActive) throw new BadRequestException('User inactive') + if (!user.hasPermission) throw new ForbiddenException('No permission') + if (!user.subscription) throw new BadRequestException('No subscription') + + return this.process(user) +} +``` + +### No Duplicate Strings +```typescript +// ❌ BAD: Duplicate strings +throw new NotFoundException('Entity not found') +throw new NotFoundException('Entity not found') +throw new NotFoundException('Entity not found') + +// ✅ GOOD: Constants +const ERROR_MESSAGES = { + ENTITY_NOT_FOUND: 'Entity not found', + INVALID_INPUT: 'Invalid input provided', + UNAUTHORIZED: 'Unauthorized access', +} as const + +throw new NotFoundException(ERROR_MESSAGES.ENTITY_NOT_FOUND) +``` + +## API Documentation (Swagger) + +### Comprehensive Documentation +```typescript +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger' + +@ApiTags('Users') +@Controller('api/users') +export class UserController { + @Get(':id') + @ApiOperation({ + summary: 'Get user by ID', + description: 'Retrieves a single user by their unique identifier' + }) + @ApiParam({ + name: 'id', + description: 'User ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000' + }) + @ApiResponse({ + status: 200, + description: 'User found', + type: UserDto + }) + @ApiResponse({ + status: 404, + description: 'User not found' + }) + async findById(@Param('id') id: string): Promise { + return this.userService.findById(id) + } + + @Get() + @ApiOperation({ summary: 'List all users' }) + @ApiQuery({ + name: 'page', + required: false, + type: Number, + description: 'Page number', + example: 1 + }) + @ApiQuery({ + name: 'limit', + required: false, + type: Number, + description: 'Items per page', + example: 10 + }) + @ApiResponse({ status: 200, type: [UserDto] }) + async findAll( + @Query('page') page = 1, + @Query('limit') limit = 10, + ): Promise { + return this.userService.findAll(page, limit) + } +} +``` + +## Best Practices + +### Logging +```typescript +// ✅ GOOD: Use NestJS Logger +private readonly logger = new Logger(ServiceName.name) + +this.logger.log('Processing started') +this.logger.debug('Debug info', { data }) +this.logger.warn('Warning message') +this.logger.error('Error occurred', error.stack) + +// ❌ BAD: console.log +console.log('Processing started') // Don't use in production +``` + +### Configuration +```typescript +// ✅ GOOD: Use ConfigService +import { ConfigService } from '@nestjs/config' + +@Injectable() +export class AppService { + constructor(private configService: ConfigService) {} + + getDatabaseUrl(): string { + return this.configService.get('DATABASE_URL') + } +} +``` + +### Guards and Interceptors +```typescript +// Guard for authentication +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' + +@Injectable() +export class AuthGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + return this.validateRequest(request) + } + + private validateRequest(request: any): boolean { + // Validation logic + return true + } +} + +// Interceptor for logging +import { CallHandler, ExecutionContext, Ninjectable, NestInterceptor } from '@nestjs/common' +import { Observable } from 'rxjs' +import { tap } from 'rxjs/operators' + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const now = Date.now() + return next + .handle() + .pipe( + tap(() => console.log(`Request took ${Date.now() - now}ms`)) + ) + } +} +``` + +## Common Pitfalls + +1. ❌ Not using dependency injection +2. ❌ Missing validation on DTOs +3. ❌ Poor error handling +4. ❌ No logging +5. ❌ Missing Swagger documentation +6. ❌ High cognitive complexity +7. ❌ Not using transactions for related operations +8. ❌ Hardcoded configuration values +9. ❌ Missing proper HTTP status codes +10. ❌ No input sanitization + +## Checklist + +- [ ] Services use dependency injection +- [ ] DTOs have validation decorators +- [ ] Controllers have Swagger documentation +- [ ] Proper HTTP status codes used +- [ ] Error handling with appropriate exceptions +- [ ] Logging for important operations +- [ ] Transactions for related DB operations +- [ ] Configuration via ConfigService +- [ ] Guards for authentication/authorization +- [ ] Cognitive complexity ≤ 15 + diff --git a/.ai/rules/04-TESTING.md b/.ai/rules/04-TESTING.md new file mode 100644 index 0000000000..d4640718ec --- /dev/null +++ b/.ai/rules/04-TESTING.md @@ -0,0 +1,720 @@ +# Testing Standards and Practices + +## General Testing Principles + +### Core Principles + +- **Write tests for all new features** - No feature is complete without tests +- **Follow AAA pattern**: Arrange, Act, Assert +- **Use descriptive test names**: "should do X when Y" +- **CRITICAL**: Never use fixed time waits with magic numbers - tests must be deterministic +- **CRITICAL**: Use faker library (@faker-js/faker) for generating random test data + +### Test Coverage Requirements + +```javascript +// jest.config.cjs - Coverage thresholds +coverageThreshold: { + global: { + statements: 80, // 80% minimum + branches: 63, // 63% minimum + functions: 72, // 72% minimum + lines: 80, // 80% minimum + }, +} +``` + +### Test Organization + +```typescript +describe('FeatureService', () => { + // Group related tests + describe('findById', () => { + it('should return entity when found', () => {}); + it('should throw NotFoundException when not found', () => {}); + }); + + describe('create', () => { + it('should create entity with valid data', () => {}); + it('should throw error with invalid data', () => {}); + }); +}); +``` + +## Frontend Testing (Jest + Testing Library) + +### Component Test Pattern + +**Important**: Create a shared `renderComponent` helper function for each component test file. This function should: + +- Accept props overrides +- Provide default props and behavior +- Handle common setup (Redux Provider, Router, etc.) +- Be reusable across all test cases + +```typescript +import React from 'react' +import { render, screen, fireEvent, waitFor, RenderResult } from '@testing-library/react' +import { Provider } from 'react-redux' +import { faker } from '@faker-js/faker' +import { MyComponent } from './MyComponent' +import { MyComponentProps } from './MyComponent.types' +import { store } from 'uiSrc/slices/store' + +describe('MyComponent', () => { + // ✅ GOOD: Shared render helper with default props + const defaultProps: MyComponentProps = { + id: faker.string.uuid(), + name: faker.person.fullName(), + email: faker.internet.email(), + onComplete: jest.fn(), + } + + const renderComponent = (propsOverride?: Partial): RenderResult => { + const props = { ...defaultProps, ...propsOverride } + + return render( + + + + ) + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render component with data', () => { + // Arrange + const name = faker.person.fullName() + const email = faker.internet.email() + + // Act + renderComponent({ name, email }) + + // Assert + expect(screen.getByText(name)).toBeInTheDocument() + expect(screen.getByText(email)).toBeInTheDocument() + }) + + it('should call onComplete when button is clicked', async () => { + // Arrange + const mockOnComplete = jest.fn() + renderComponent({ onComplete: mockOnComplete }) + + // Act + const button = screen.getByRole('button', { name: /submit/i }) + fireEvent.click(button) + + // Assert - ✅ GOOD: Use waitFor for async behavior + await waitFor(() => { + expect(mockOnComplete).toHaveBeenCalledTimes(1) + }) + }) + + it('should display error message on failure', async () => { + // Arrange + const errorMessage = faker.lorem.sentence() + + // Act + renderComponent({ error: errorMessage }) + + // Assert - ✅ GOOD: Use waitFor instead of fixed timeout + await waitFor(() => { + expect(screen.getByText(errorMessage)).toBeInTheDocument() + }) + }) + + it('should handle loading state', () => { + // Act + renderComponent({ loading: true }) + + // Assert + expect(screen.getByText(/loading/i)).toBeInTheDocument() + }) +}) +``` + +### Complex Component Setup Example + +For components requiring more complex setup (Router, theme, etc.): + +```typescript +import React from 'react' +import { render, RenderResult } from '@testing-library/react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import { ThemeProvider } from 'styled-components/macro' +import { faker } from '@faker-js/faker' +import { UserProfile } from './UserProfile' +import { UserProfileProps } from './UserProfile.types' +import { store } from 'uiSrc/slices/store' +import { theme } from 'uiSrc/styles/theme' + +describe('UserProfile', () => { + const defaultProps: UserProfileProps = { + userId: faker.string.uuid(), + onUpdate: jest.fn(), + onDelete: jest.fn(), + } + + const renderComponent = (propsOverride?: Partial): RenderResult => { + const props = { ...defaultProps, ...propsOverride } + + return render( + + + + + + + + ) + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render user profile', () => { + renderComponent() + expect(screen.getByTestId('user-profile')).toBeInTheDocument() + }) +}) +``` + +### Testing with Redux Store + +For components with Redux state, create a test store: + +```typescript +import { configureStore } from '@reduxjs/toolkit' +import { faker } from '@faker-js/faker' +import { userSlice } from 'uiSrc/slices/user' + +describe('ConnectedComponent', () => { + const createTestStore = (initialState = {}) => { + return configureStore({ + reducer: { + user: userSlice.reducer, + }, + preloadedState: initialState, + }) + } + + const renderComponent = (propsOverride?: Partial, storeState = {}) => { + const testStore = createTestStore(storeState) + const props = { ...defaultProps, ...propsOverride } + + return render( + + + + ) + } + + it('should display user from Redux store', () => { + const userName = faker.person.fullName() + + renderComponent({}, { + user: { + data: { name: userName }, + loading: false, + }, + }) + + expect(screen.getByText(userName)).toBeInTheDocument() + }) +}) +``` + +### Query Priorities (Testing Library) + +```typescript +// ✅ PREFERRED: Accessible queries (as users would interact) +screen.getByRole('button', { name: /submit/i }); +screen.getByLabelText('Email'); +screen.getByPlaceholderText('Enter name'); +screen.getByText('Welcome'); + +// ✅ ACCEPTABLE: Semantic queries +screen.getByAltText('Profile picture'); +screen.getByTitle('Close dialog'); + +// ⚠️ LAST RESORT: test IDs +screen.getByTestId('user-profile'); + +// ❌ AVOID: Implementation details +wrapper.find('.button-class'); +wrapper.instance().method(); +``` + +### Testing Async Behavior + +```typescript +// ✅ GOOD: waitFor with proper queries +await waitFor(() => { + expect(screen.getByText('Data loaded')).toBeInTheDocument(); +}); + +// ✅ GOOD: waitForElementToBeRemoved +await waitForElementToBeRemoved(() => screen.queryByText('Loading...')); + +// ✅ GOOD: findBy queries (built-in waiting) +const element = await screen.findByText('Async content'); + +// ❌ BAD: Fixed timeouts (flaky tests) +await new Promise((resolve) => setTimeout(resolve, 1000)); +expect(screen.getByText('Data')).toBeInTheDocument(); +``` + +### Mocking API Calls (MSW) + +```typescript +import { rest } from 'msw' +import { setupServer } from 'msw/node' +import { faker } from '@faker-js/faker' + +// Setup mock server +const server = setupServer( + rest.get('/api/users/:id', (req, res, ctx) => { + return res( + ctx.json({ + id: req.params.id, + name: faker.person.fullName(), + email: faker.internet.email(), + }) + ) + }) +) + +beforeAll(() => server.listen()) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + +it('should fetch and display user data', async () => { + const userId = faker.string.uuid() + + render() + + await waitFor(() => { + expect(screen.getByRole('heading')).toBeInTheDocument() + }) +}) +``` + +### Testing Redux + +```typescript +import { renderHook, act } from '@testing-library/react-hooks'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { faker } from '@faker-js/faker'; +import { userSlice, fetchUser } from './userSlice'; + +describe('userSlice', () => { + let store; + + beforeEach(() => { + store = configureStore({ + reducer: { user: userSlice.reducer }, + }); + }); + + it('should handle fetchUser.fulfilled', () => { + const user = { + id: faker.string.uuid(), + name: faker.person.fullName(), + }; + + store.dispatch(fetchUser.fulfilled(user, '', user.id)); + + expect(store.getState().user.data).toEqual(user); + expect(store.getState().user.loading).toBe(false); + }); +}); +``` + +## Backend Testing (NestJS/Jest) + +### Service Test Pattern + +```typescript +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { faker } from '@faker-js/faker'; +import { NotFoundException } from '@nestjs/common'; +import { UserService } from './user.service'; +import { User } from './entities/user.entity'; + +describe('UserService', () => { + let service: UserService; + let repository: Repository; + + const mockRepository = { + find: jest.fn(), + findOne: jest.fn(), + save: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + create: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserService, + { + provide: getRepositoryToken(User), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get(UserService); + repository = module.get>(getRepositoryToken(User)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('findById', () => { + it('should return user when found', async () => { + // Arrange + const userId = faker.string.uuid(); + const mockUser = { + id: userId, + name: faker.person.fullName(), + email: faker.internet.email(), + }; + mockRepository.findOne.mockResolvedValue(mockUser); + + // Act + const result = await service.findById(userId); + + // Assert + expect(result).toEqual(mockUser); + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + }); + }); + + it('should throw NotFoundException when user not found', async () => { + // Arrange + const userId = faker.string.uuid(); + mockRepository.findOne.mockResolvedValue(null); + + // Act & Assert + await expect(service.findById(userId)).rejects.toThrow(NotFoundException); + }); + }); + + describe('create', () => { + it('should create and return user', async () => { + // Arrange + const createDto = { + name: faker.person.fullName(), + email: faker.internet.email(), + }; + const mockUser = { id: faker.string.uuid(), ...createDto }; + + mockRepository.create.mockReturnValue(mockUser); + mockRepository.save.mockResolvedValue(mockUser); + + // Act + const result = await service.create(createDto); + + // Assert + expect(result).toEqual(mockUser); + expect(mockRepository.create).toHaveBeenCalledWith(createDto); + expect(mockRepository.save).toHaveBeenCalledWith(mockUser); + }); + }); +}); +``` + +### Controller Test Pattern + +```typescript +import { Test, TestingModule } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; + +describe('UserController', () => { + let controller: UserController; + let service: UserService; + + const mockService = { + findAll: jest.fn(), + findById: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [ + { + provide: UserService, + useValue: mockService, + }, + ], + }).compile(); + + controller = module.get(UserController); + service = module.get(UserService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('findById', () => { + it('should return user from service', async () => { + const userId = faker.string.uuid(); + const mockUser = { + id: userId, + name: faker.person.fullName(), + email: faker.internet.email(), + }; + + mockService.findById.mockResolvedValue(mockUser); + + const result = await controller.findById(userId); + + expect(result).toEqual(mockUser); + expect(mockService.findById).toHaveBeenCalledWith(userId); + }); + }); +}); +``` + +### Integration Tests (E2E - API) + +```typescript +import * as request from 'supertest'; +import { Test } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { faker } from '@faker-js/faker'; +import { AppModule } from '../src/app.module'; + +describe('UserController (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('/users (GET)', () => { + it('should return array of users', () => { + return request(app.getHttpServer()) + .get('/users') + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + }); + }); + }); + + describe('/users (POST)', () => { + it('should create new user', () => { + const createDto = { + name: faker.person.fullName(), + email: faker.internet.email(), + }; + + return request(app.getHttpServer()) + .post('/users') + .send(createDto) + .expect(201) + .expect((res) => { + expect(res.body.name).toBe(createDto.name); + expect(res.body.email).toBe(createDto.email); + expect(res.body.id).toBeDefined(); + }); + }); + + it('should return 400 for invalid data', () => { + return request(app.getHttpServer()) + .post('/users') + .send({ name: '' }) // Missing required email + .expect(400); + }); + }); +}); +``` + +## E2E Testing (Playwright) + +### E2E Test Pattern + +```typescript +import { test, expect } from '@playwright/test'; +import { faker } from '@faker-js/faker'; + +test.describe('User Management', () => { + test('should create and display new user', async ({ page }) => { + // Arrange + const userName = faker.person.fullName(); + const userEmail = faker.internet.email(); + + // Act + await page.goto('/users'); + await page.click('text=Add User'); + await page.fill('[name="name"]', userName); + await page.fill('[name="email"]', userEmail); + await page.click('text=Submit'); + + // Assert - Use proper waits, not timeouts + await expect(page.locator(`text=${userName}`)).toBeVisible(); + await expect(page.locator(`text=${userEmail}`)).toBeVisible(); + }); + + test('should edit existing user', async ({ page }) => { + const newName = faker.person.fullName(); + + await page.goto('/users'); + await page.click('[data-test="edit-button"]:first-child'); + await page.fill('[name="name"]', newName); + await page.click('text=Save'); + + // ✅ GOOD: Wait for element + await page.waitForSelector(`text=${newName}`); + await expect(page.locator(`text=${newName}`)).toBeVisible(); + }); +}); +``` + +## Testing Best Practices + +### Use Faker for Test Data + +```typescript +// ✅ GOOD: Use faker for random data +const user = { + id: faker.string.uuid(), + name: faker.person.fullName(), + email: faker.internet.email(), + age: faker.number.int({ min: 18, max: 100 }), + address: faker.location.streetAddress(), +}; + +// ❌ BAD: Hardcoded test data +const user = { + id: '123', + name: 'Test User', + email: 'test@example.com', +}; +``` + +### No Fixed Timeouts + +```typescript +// ❌ BAD: Fixed timeout (flaky) +await new Promise((resolve) => setTimeout(resolve, 1000)); +expect(element).toBeInTheDocument(); + +// ❌ BAD: Magic number timeout +await page.waitForTimeout(2000); + +// ✅ GOOD: Wait for specific condition +await waitFor(() => { + expect(element).toBeInTheDocument(); +}); + +// ✅ GOOD: Playwright built-in waiting +await page.waitForSelector('[data-test="result"]'); +await expect(page.locator('[data-test="result"]')).toBeVisible(); +``` + +### Mock External Dependencies + +```typescript +// ✅ GOOD: Mock external services +jest.mock('uiSrc/services/api', () => ({ + apiService: { + get: jest.fn(), + post: jest.fn(), + }, +})); + +// ✅ GOOD: Use jest-when for complex scenarios +import { when } from 'jest-when'; + +when(mockService.findById) + .calledWith('user-1') + .mockResolvedValue({ id: 'user-1', name: 'User 1' }) + .calledWith('user-2') + .mockResolvedValue({ id: 'user-2', name: 'User 2' }); +``` + +### Test Edge Cases + +```typescript +describe('calculateTotal', () => { + it('should handle empty array', () => { + expect(calculateTotal([])).toBe(0); + }); + + it('should handle single item', () => { + expect(calculateTotal([10])).toBe(10); + }); + + it('should handle negative numbers', () => { + expect(calculateTotal([10, -5])).toBe(5); + }); + + it('should handle large numbers', () => { + expect(calculateTotal([Number.MAX_SAFE_INTEGER, 1])).toBeDefined(); + }); + + it('should handle null/undefined gracefully', () => { + expect(() => calculateTotal(null)).not.toThrow(); + }); +}); +``` + +## Common Testing Pitfalls + +1. ❌ Using fixed timeouts instead of waitFor +2. ❌ Hardcoding test data instead of using faker +3. ❌ Testing implementation details instead of behavior +4. ❌ Not cleaning up mocks between tests +5. ❌ Missing edge case tests +6. ❌ Not testing error scenarios +7. ❌ Shallow testing without integration tests +8. ❌ No E2E tests for critical flows +9. ❌ Forgetting to assert in async tests +10. ❌ Not using proper queries (getByRole, etc.) +11. ❌ Calling `render` directly in tests instead of using shared `renderComponent` helper +12. ❌ Duplicating setup code across test cases + +## Testing Checklist + +- [ ] All new features have tests +- [ ] Tests use faker for data generation +- [ ] No fixed timeouts (use waitFor) +- [ ] Tests follow AAA pattern +- [ ] Descriptive test names +- [ ] Shared `renderComponent` helper used (no direct `render` calls) +- [ ] Default props defined for component tests +- [ ] Edge cases covered +- [ ] Error scenarios tested +- [ ] Mocks cleaned up between tests +- [ ] Integration tests for API endpoints +- [ ] E2E tests for critical flows +- [ ] Coverage meets thresholds (80%+) diff --git a/.ai/rules/05-WORKFLOW.md b/.ai/rules/05-WORKFLOW.md new file mode 100644 index 0000000000..61c9b3ea38 --- /dev/null +++ b/.ai/rules/05-WORKFLOW.md @@ -0,0 +1,696 @@ +# Development Workflow and Git Practices + +## Git Workflow + +### Branch Naming + +Use lowercase kebab-case with type prefix and issue/ticket identifier: + +```bash +# Pattern: // + +# INTERNAL DEVELOPMENT (JIRA Tickets - #RI-XXX) +# Feature branches +feature/RI-123/add-user-profile +feature/RI-456/redis-cluster-support + +# Bug fixes +bugfix/RI-789/memory-leak-connections +bugfix/RI-234/ui-rendering-issue + +# Frontend-only features +fe/feature/RI-567/add-dark-mode +fe/feature/RI-890/user-profile-editor + +# Backend-only fixes +be/bugfix/RI-345/update-databases-api +be/bugfix/RI-678/fix-redis-connection + +# OPEN SOURCE CONTRIBUTIONS (GitHub Issues - XXX) +# Feature branches +feature/123/add-export-feature +feature/456/improve-performance + +# Bug fixes +bugfix/789/fix-connection-timeout +bugfix/234/handle-edge-case + +# Frontend-only features +fe/feature/567/improve-accessibility +fe/feature/890/add-keyboard-shortcuts + +# Backend-only fixes +be/bugfix/#345/fix-cluster-discovery +be/bugfix/#678/handle-connection-error + +# Hotfixes (no ticket required for critical production issues) +hotfix/critical-security-patch + +# Refactoring +refactor/RI-111/extract-redis-service +refactor/222/simplify-validation + +# Documentation +docs/RI-333/update-api-documentation +docs/444/improve-readme + +# Chores/maintenance +chore/RI-555/upgrade-dependencies +chore/666/update-ci-pipeline +chore/update-eslint-config # Can omit ticket for minor maintenance +``` + +**Where:** + +- `RI-XXX` = JIRA ticket number (e.g., RI-123) - for internal development +- `XXX` = GitHub issue number (e.g., 456) - for open source contributions +- Note: Use `#` only in commit message references (e.g., `Fixes #456`), not in branch names + +### Commit Messages + +Follow **Conventional Commits** format: + +``` +(): + + + +