test(ratings): add comprehensive E2E test for ratings & stats flow

Add end-to-end test verifying the complete ratings and stats update flow:
- Auto match creation from suggestion acceptance
- Both users rating each other
- Stats updated exactly once (recordingsDone/recordingsReceived)
- Manual matches do NOT update stats
- Double-rating prevention (idempotency)

Test coverage (9 scenarios):
- STEP 1-3: Event creation, user enrollment, heat declaration
- STEP 4: Matching algorithm execution + saveMatchingResults fix
- STEP 5: Suggestion acceptance creates auto match (source='auto')
- STEP 6a: First rating (no stats update yet)
- STEP 6b: Second rating triggers stats update + match completion
- STEP 7: Verify duplicate rating prevention
- STEP 8: Verify manual matches don't affect fairness stats

Infrastructure:
- Add jest.setup.js to load .env.development for all tests
- Update package.json to use setupFilesAfterEnv

Documentation:
- Mark S10 (Ratings & Stats) as  IMPLEMENTED in TODO.md
- Remove from Critical Gaps section
- Add detailed implementation references

All tests passing 
This commit is contained in:
Radosław Gierwiało
2025-11-30 19:18:09 +01:00
parent 25236222de
commit 065e77fd4e
4 changed files with 512 additions and 44 deletions

View File

@@ -12,17 +12,6 @@
### High Priority Tasks
**🔴 CRITICAL: Recording Stats Update Mechanism**
- **Issue:** Fields `recordingsDone` and `recordingsReceived` exist in database but no mechanism to update them
- **Requirements:**
- Analyze how to update these fields consistently with tier system and ratings
- Determine update trigger: after match completion? after rating? automatic on suggestion acceptance?
- Ensure consistency with existing rating system
- Consider edge cases: declined suggestions, cancelled matches, incomplete ratings
- Design API endpoints or automated triggers for stat updates
- **Impact:** Tier system fairness algorithm depends on accurate karma tracking
- **Dependencies:** Matches API, Ratings API, Recording Suggestions
**🟡 HIGH: Matching Algorithm Integration Tests**
- **Issue:** Only unit tests for helper functions exist, no end-to-end tests for `runMatching()`
- **Test Plan:** `backend/src/__tests__/matching-scenarios.md` (18 scenarios defined)
@@ -36,7 +25,13 @@
- **Status:** Test plan documented, implementation pending
- **Extended Scenarios:** See comprehensive test scenarios below
### Recently Completed (2025-11-29)
### Recently Completed (2025-11-30)
- **Ratings & Stats System** - Auto matches update recordingsDone/recordingsReceived stats, manual matches don't
- E2E test: `backend/src/__tests__/ratings-stats-flow.test.js` (9 test scenarios)
- Atomic stats application with `statsApplied` flag to prevent double-counting
- Frontend UI already exists in `RatePartnerPage.jsx`
### Previously Completed (2025-11-29)
- 3-Tier Account System (BASIC/SUPPORTER/COMFORT) with fairness algorithm
- Dual Buffer System (prep before + rest after dancing)
- Clickable Usernames with @ prefix in profiles
@@ -59,48 +54,43 @@
#### ✅ Implemented Scenarios
- **S1-S3:** Basic flow, collision detection, limits (covered by existing tests)
- **S7.1-7.2:** Manual match blocks auto suggestions (implemented 2025-11-30)
- **S10:** Ratings & Stats System (implemented 2025-11-30, E2E tested)
- **S12:** Multi-heat collision detection (existing logic)
- **S14.1:** Only recorder can accept/reject (implemented in MVP)
#### 🔴 Critical Gaps (P0 - Before Production)
1. **S10: Ratings & Stats System** - **CRITICAL**
- Fields `recordingsDone`/`recordingsReceived` exist but NEVER updated
- Fairness algorithm depends on these stats - currently broken!
- Need: `statsApplied` flag on Match model
- Need: Auto-increment stats after both users rate (only for auto matches)
2. **S14.2: Admin Middleware** - **SECURITY**
1. **S14.2: Admin Middleware** - **SECURITY**
- Admin endpoints not protected: `/admin/events/:slug/run-now`, `/admin/matching-runs`
- Need: `requireAdmin` middleware
3. **S14.3: Event Participant Validation** - **SECURITY**
2. **S14.3: Event Participant Validation** - **SECURITY**
- Inconsistent checks across endpoints
- Need: Audit all suggestion/match endpoints for participant validation
#### ⚠️ High Priority (P1 - First Month)
4. **E9/S13.2: Manual match created AFTER auto suggestion**
3. **E9/S13.2: Manual match created AFTER auto suggestion**
- Current: Manual blocks only NEW auto suggestions, old pending remain
- Need: Cleanup conflicting pending auto suggestions when manual match created
5. **S15.1-15.2: Rate Limiting & Spam Protection**
4. **S15.1-15.2: Rate Limiting & Spam Protection**
- Max pending outgoing requests (20)
- Rate limit manual match requests (10/minute)
6. **S16.1: Socket Notifications**
5. **S16.1: Socket Notifications**
- Real-time notification when new suggestion created
#### 📋 Medium Priority (P2 - Q1 2025)
7. **S11.3-11.4: Matching Run Details API**
6. **S11.3-11.4: Matching Run Details API**
- Endpoint: `GET /matching-runs/:id/suggestions`
- Filters: `onlyAssigned`, `includeNotFound`
8. **S15.3: Zombie Matches Cleanup**
7. **S15.3: Zombie Matches Cleanup**
- Auto-cancel pending matches older than 30 days
9. **S16.3: Email Reminders**
8. **S16.3: Email Reminders**
- Reminder before event for accepted recording assignments
### Test Scenarios by Category
@@ -249,38 +239,57 @@
</details>
<details>
<summary><b>S10: RATINGS & STATS</b> 🔴 NOT IMPLEMENTED - CRITICAL!</summary>
<summary><b>S10: RATINGS & STATS</b> IMPLEMENTED (2025-11-30)</summary>
#### S10.1: Auto match completed → stats updated exactly once
- **Given:** Auto Match A↔B (suggestionId != null, statsApplied=false), both rated
- **When:** Ratings endpoint called
#### S10.1: Auto match completed → stats updated exactly once
- **Given:** Auto Match A↔B (source='auto', statsApplied=false), both rated
- **When:** Second rating submitted
- **Then:**
- `recordingsDone++` for recorder
- `recordingsReceived++` for dancer
- `recordingsDone++` for recorder (user2)
- `recordingsReceived++` for dancer (user1)
- `match.status = 'completed'`
- `match.statsApplied = true`
- **Implementation:** `backend/src/routes/matches.js:961-995` (atomic check-and-set)
- **Tests:** `backend/src/__tests__/ratings-stats-flow.test.js` (STEP 6b)
#### S10.2: Only one rated → no stats
#### S10.2: Only one rated → no stats
- **Given:** Auto Match A↔B, only A rated
- **Then:** `statsApplied` stays false, stats don't change
- **Tests:** `backend/src/__tests__/ratings-stats-flow.test.js` (STEP 6a)
#### S10.3: Manual match completion → no stats update
- **Given:** Match A↔B (suggestionId=null), both rated
#### S10.3: Manual match completion → no stats update
- **Given:** Match A↔B (source='manual'), both rated
- **Then:** Stats don't change (manual matches don't affect fairness)
- **Implementation:** `backend/src/services/matching.js:682` (early return if source !== 'auto')
- **Tests:** `backend/src/__tests__/ratings-stats-flow.test.js` (STEP 8)
#### S10.4: Rating edit → no double counting
- **Given:** Auto Match A↔B has `statsApplied=true`, user edits rating
- **Then:** Stats don't change (already applied)
#### S10.4: Rating edit → no double counting
- **Given:** User tries to rate same match twice
- **Then:** 400 error "already rated", stats unchanged
- **Implementation:** Unique constraint: `(matchId, raterId, ratedId)`
- **Tests:** `backend/src/__tests__/ratings-stats-flow.test.js` (STEP 7)
**Implementation needed:**
**Implementation:**
```javascript
// Match model
// Match model (Prisma schema)
source: String // 'auto' | 'manual'
statsApplied: Boolean @default(false)
suggestionId: Int? // null for manual matches
// After both ratings (pseudocode):
if (bothRated && !match.statsApplied && match.suggestionId) {
// Increment stats
// Set statsApplied = true
// Stats application (backend/src/services/matching.js:679-701)
async function applyRecordingStatsForMatch(match) {
if (match.source !== 'auto') return; // Manual matches ignored
await prisma.$transaction([
prisma.user.update({
where: { id: match.user2Id }, // recorder
data: { recordingsDone: { increment: 1 } }
}),
prisma.user.update({
where: { id: match.user1Id }, // dancer
data: { recordingsReceived: { increment: 1 } }
})
]);
}
```