Skip to content

Latest commit

 

History

History
541 lines (421 loc) · 15 KB

File metadata and controls

541 lines (421 loc) · 15 KB

Issue #207: EscrowCheckWorker Disputes Query Fix

Problem Summary

The EscrowCheckWorker eligibility query incorrectly referenced d.escrow_id when checking for active disputes, but the disputes table schema uses transaction_id as the foreign key to the escrows table. This caused the NOT EXISTS subquery to always return false (no rows matched), allowing escrows with active disputes to be incorrectly auto-released.

Root Cause

Incorrect Query (Before Fix):

SELECT e.id
FROM escrows e
WHERE e.status = 'active'
  AND e.session_end_time < NOW() - INTERVAL '72 hours'
  AND NOT EXISTS (
    SELECT 1 FROM disputes d
    WHERE d.escrow_id = e.id  -- ❌ Column doesn't exist
      AND d.status NOT IN ('resolved', 'closed')
  )

Database Schema:

-- disputes table uses transaction_id, not escrow_id
CREATE TABLE disputes (
  id SERIAL PRIMARY KEY,
  transaction_id INTEGER REFERENCES escrows(id),  -- ✅ Correct FK column
  status VARCHAR(50),
  created_at TIMESTAMP
);

Impact

  • Security Risk: Escrows with active disputes were being auto-released to mentors
  • Data Integrity: Dispute resolution process was bypassed
  • User Trust: Learners could lose funds while disputes were pending

Solution

1. Fixed Query

Corrected Query (After Fix):

SELECT e.id
FROM escrows e
WHERE e.status = 'active'
  AND e.session_end_time < NOW() - INTERVAL '72 hours'
  AND NOT EXISTS (
    SELECT 1 FROM disputes d
    WHERE d.transaction_id = e.id  -- ✅ Correct column reference
      AND d.status NOT IN ('resolved', 'closed')
  )

2. Implementation

File: mentorminds-backend/src/jobs/escrowCheck.worker.ts

import { Pool } from 'pg';

export interface EscrowAutoReleaseService {
  tryAutoRelease(escrowId: number): Promise<{ hash: string }>;
}

export class EscrowCheckWorker {
  constructor(
    private readonly pool: Pool,
    private readonly autoReleaseService: EscrowAutoReleaseService
  ) {}

  async processEligibleEscrows(): Promise<void> {
    const eligibleEscrows = await this.pool.query<{ id: number }>(
      `SELECT e.id
       FROM escrows e
       WHERE e.status = 'active'
         AND e.session_end_time < NOW() - INTERVAL '72 hours'
         AND NOT EXISTS (
           SELECT 1 FROM disputes d
           WHERE d.transaction_id = e.id
             AND d.status NOT IN ('resolved', 'closed')
         )`,
      []
    );

    for (const row of eligibleEscrows.rows) {
      try {
        await this.autoReleaseService.tryAutoRelease(row.id);
      } catch (err) {
        console.error(`Auto-release failed for escrow ${row.id}:`, err);
      }
    }
  }
}

3. Integration Tests

File: mentorminds-backend/tests/escrowCheck.worker.test.ts

Comprehensive test suite covering:

Test Cases

  1. Active Dispute Block

    • Escrow with status = 'open' dispute should NOT auto-release
    • Verifies the dispute guard is working
  2. Resolved Dispute Allow

    • Escrow with status = 'resolved' dispute SHOULD auto-release
    • Confirms resolved disputes don't block release
  3. Closed Dispute Allow

    • Escrow with status = 'closed' dispute SHOULD auto-release
    • Confirms closed disputes don't block release
  4. No Dispute Allow

    • Escrow with no disputes SHOULD auto-release
    • Baseline happy path
  5. Pending Dispute Block

    • Escrow with status = 'pending' dispute should NOT auto-release
    • Covers additional dispute states
  6. Under Review Dispute Block

    • Escrow with status = 'under_review' dispute should NOT auto-release
    • Covers additional dispute states

Running Tests

cd mentorminds-backend
npm test -- escrowCheck.worker.test.ts

Testing Summary

Test Execution Results

✅ Mock Mode Tests (Fast Unit Tests)

cd mentorminds-backend
npm test -- escrowCheck.worker.test.ts

Results:

PASS tests/escrowCheck.worker.test.ts
  EscrowCheckWorker - Dispute Guard
    ✓ should NOT auto-release escrow with active dispute (4 ms)
    ✓ should auto-release escrow with resolved dispute (1 ms)
    ✓ should auto-release escrow with closed dispute (1 ms)
    ✓ should auto-release escrow with no disputes
    ✓ should NOT auto-release escrow with pending dispute (1 ms)
    ✓ should NOT auto-release escrow with under_review dispute (1 ms)
    ✓ should process multiple eligible escrows (1 ms)
    ✓ should continue processing if one auto-release fails (1 ms)
    ✓ should use correct SQL query with transaction_id column

Test Suites: 1 passed, 1 total
Tests:       9 passed, 9 total
Time:        2.78 s

Status: ✅ All tests passing

Test Coverage

Test Case Purpose Status
Active dispute blocks auto-release Verifies disputed escrows are NOT released ✅ Pass
Resolved dispute allows auto-release Confirms resolved disputes don't block ✅ Pass
Closed dispute allows auto-release Confirms closed disputes don't block ✅ Pass
No dispute allows auto-release Baseline happy path ✅ Pass
Pending dispute blocks auto-release Covers additional dispute states ✅ Pass
Under review dispute blocks Covers additional dispute states ✅ Pass
Multiple escrows processed Batch processing works correctly ✅ Pass
Error handling Worker continues on individual failures ✅ Pass
Correct SQL column Verifies transaction_id is used ✅ Pass

Test Modes

The test suite supports two execution modes:

1. Mock Mode (Default)

  • Speed: Fast (~3 seconds)
  • Dependencies: None (no database required)
  • Use Case: Development, CI/CD, quick validation
  • Command: npm test -- escrowCheck.worker.test.ts

2. Integration Mode (Real Database)

  • Speed: Slower (~10-15 seconds)
  • Dependencies: PostgreSQL database
  • Use Case: Pre-deployment validation, comprehensive testing
  • Command: DATABASE_URL=postgresql://... npm test -- escrowCheck.worker.test.ts

How to Run Tests

Quick Test (Mock Mode)

cd mentorminds-backend
npm test -- escrowCheck.worker.test.ts

Full Integration Test (Docker)

# Start test database
docker run -d --name test-db \
  -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test \
  -e POSTGRES_DB=test -p 5433:5432 postgres:15

# Run tests
DATABASE_URL=postgresql://test:test@localhost:5433/test \
  npm test -- escrowCheck.worker.test.ts

# Cleanup
docker stop test-db && docker rm test-db

Watch Mode (Development)

npm test -- escrowCheck.worker.test.ts --watch

Verification Steps

1. Database Schema Verification

Confirm the disputes table structure:

\d disputes

-- Expected output should show:
-- transaction_id | integer | references escrows(id)

2. Run Unit Tests (Mock Mode)

The tests run in mock mode by default, using mocked database queries for fast execution:

cd mentorminds-backend
npm test -- escrowCheck.worker.test.ts

Expected output:

PASS  tests/escrowCheck.worker.test.ts
  EscrowCheckWorker - Dispute Guard
    ✓ should NOT auto-release escrow with active dispute
    ✓ should auto-release escrow with resolved dispute
    ✓ should auto-release escrow with closed dispute
    ✓ should auto-release escrow with no disputes
    ✓ should NOT auto-release escrow with pending dispute
    ✓ should NOT auto-release escrow with under_review dispute
    ✓ should process multiple eligible escrows
    ✓ should continue processing if one auto-release fails
    ✓ should use correct SQL query with transaction_id column

Test Suites: 1 passed, 1 total
Tests:       9 passed, 9 total

3. Run Integration Tests (Real Database Mode)

For comprehensive testing with a real PostgreSQL database:

Option A: Using Docker (Recommended)

# Start PostgreSQL test database
docker run -d \
  --name mentorminds-test-db \
  -e POSTGRES_USER=testuser \
  -e POSTGRES_PASSWORD=testpass \
  -e POSTGRES_DB=mentorminds_test \
  -p 5433:5432 \
  postgres:15

# Wait for database to be ready
sleep 5

# Run tests with real database
DATABASE_URL=postgresql://testuser:testpass@localhost:5433/mentorminds_test \
  npm test -- escrowCheck.worker.test.ts

# Clean up
docker stop mentorminds-test-db
docker rm mentorminds-test-db

Option B: Using Local PostgreSQL

# Create test database
psql -U postgres -c "CREATE DATABASE mentorminds_test;"

# Run tests
DATABASE_URL=postgresql://postgres:password@localhost:5432/mentorminds_test \
  npm test -- escrowCheck.worker.test.ts

# Clean up
psql -U postgres -c "DROP DATABASE mentorminds_test;"

Option C: Using Existing Database

# Use your existing development database
DATABASE_URL=postgresql://user:password@localhost:5432/mentorminds_dev \
  npm test -- escrowCheck.worker.test.ts

Note: Integration tests automatically create and drop test tables, so they won't interfere with existing data.

4. Manual Query Test

Test the corrected query directly in your database:

-- Create test data
INSERT INTO escrows (id, status, session_end_time, learner_id, mentor_id, amount)
VALUES (999, 'active', NOW() - INTERVAL '73 hours', 'test-learner', 'test-mentor', 100);

INSERT INTO disputes (transaction_id, status, created_at)
VALUES (999, 'open', NOW());

-- Run the corrected query
SELECT e.id
FROM escrows e
WHERE e.status = 'active'
  AND e.session_end_time < NOW() - INTERVAL '72 hours'
  AND NOT EXISTS (
    SELECT 1 FROM disputes d
    WHERE d.transaction_id = e.id
      AND d.status NOT IN ('resolved', 'closed')
  );

-- Expected: Should return 0 rows (escrow 999 has active dispute)

-- Test with resolved dispute
UPDATE disputes SET status = 'resolved' WHERE transaction_id = 999;

-- Run query again
SELECT e.id
FROM escrows e
WHERE e.status = 'active'
  AND e.session_end_time < NOW() - INTERVAL '72 hours'
  AND NOT EXISTS (
    SELECT 1 FROM disputes d
    WHERE d.transaction_id = e.id
      AND d.status NOT IN ('resolved', 'closed')
  );

-- Expected: Should return 1 row (escrow 999, dispute is resolved)

-- Clean up
DELETE FROM disputes WHERE transaction_id = 999;
DELETE FROM escrows WHERE id = 999;

5. Continuous Integration Testing

Add to your CI/CD pipeline (e.g., GitHub Actions):

name: Test EscrowCheckWorker

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: mentorminds_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        working-directory: mentorminds-backend
        run: npm install
      
      - name: Run unit tests (mock mode)
        working-directory: mentorminds-backend
        run: npm test -- escrowCheck.worker.test.ts
      
      - name: Run integration tests (real DB)
        working-directory: mentorminds-backend
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/mentorminds_test
        run: npm test -- escrowCheck.worker.test.ts

Deployment Checklist

  • Review code changes
  • Run unit tests locally
  • Run integration tests locally
  • Verify database schema matches expectations
  • Deploy to staging environment
  • Run smoke tests on staging
  • Monitor staging for 24 hours
  • Deploy to production
  • Monitor production metrics
  • Verify no disputed escrows are auto-released

Monitoring

Key Metrics to Watch

  1. Auto-Release Success Rate

    SELECT 
      COUNT(*) FILTER (WHERE status = 'released') as released,
      COUNT(*) FILTER (WHERE status = 'active') as still_active
    FROM escrows
    WHERE session_end_time < NOW() - INTERVAL '72 hours';
  2. Disputed Escrows Not Released

    SELECT e.id, e.status, d.status as dispute_status
    FROM escrows e
    JOIN disputes d ON d.transaction_id = e.id
    WHERE e.session_end_time < NOW() - INTERVAL '72 hours'
      AND e.status = 'active'
      AND d.status NOT IN ('resolved', 'closed');
  3. Worker Execution Logs

    • Monitor for errors in EscrowCheckWorker.processEligibleEscrows()
    • Alert on repeated auto-release failures

Rollback Plan

If issues are detected:

  1. Immediate: Disable the worker cron job

    # Stop the worker process
    pm2 stop escrow-check-worker
  2. Revert: Deploy previous version

    git revert <commit-hash>
    git push origin main
  3. Manual Review: Check any escrows released during the incident

    SELECT * FROM escrows
    WHERE status = 'released'
      AND released_at > '<deployment-timestamp>'
      AND id IN (
        SELECT transaction_id FROM disputes
        WHERE status NOT IN ('resolved', 'closed')
      );

Related Files

  • mentorminds-backend/src/jobs/escrowCheck.worker.ts - Worker implementation
  • mentorminds-backend/tests/escrowCheck.worker.test.ts - Integration tests
  • mentorminds-backend/src/services/escrow.service.ts - Escrow service
  • mentorminds-backend/src/models/escrow.model.ts - Escrow model

References

  • Issue: #207
  • Database Schema: migrations/010_create_disputes.sql
  • Escrow Contract: contracts/escrow/src/lib.rs
  • State Machine Docs: docs/state-machines.md

Security Considerations

Before Fix

  • ❌ Disputed escrows could be auto-released
  • ❌ Learners could lose funds during active disputes
  • ❌ Dispute resolution process could be bypassed

After Fix

  • ✅ Disputed escrows are blocked from auto-release
  • ✅ Only escrows with resolved/closed disputes can auto-release
  • ✅ Dispute resolution process is enforced
  • ✅ Comprehensive test coverage ensures correctness

Performance Impact

  • Query Performance: No significant change (both queries use indexed columns)
  • Worker Execution Time: No change (same number of rows processed)
  • Database Load: No change (same query complexity)

Future Improvements

  1. Add Database Constraint

    -- Prevent auto-release at database level
    ALTER TABLE escrows ADD CONSTRAINT no_release_with_active_dispute
    CHECK (
      status != 'released' OR NOT EXISTS (
        SELECT 1 FROM disputes
        WHERE transaction_id = escrows.id
          AND status NOT IN ('resolved', 'closed')
      )
    );
  2. Add Monitoring Dashboard

    • Real-time view of escrows pending auto-release
    • Alert on disputed escrows approaching auto-release window
  3. Add Audit Log

    • Log all auto-release attempts
    • Track why escrows were skipped (dispute, time, status)

Conclusion

This fix ensures that escrows with active disputes are never auto-released, protecting learners and maintaining the integrity of the dispute resolution process. The comprehensive test suite provides confidence that the fix works correctly across all dispute states.