agmission/Development/server/docs/archived/CLEANUP_HOOKS_FIX.md

284 lines
7.9 KiB
Markdown

# Mocha Test Cleanup Hooks Fix
## Problem Statement
After converting 64 test files to Mocha format, we discovered that **promo tests were creating Stripe resources without proper cleanup**. The cleanup code existed but was inside the test logic, so if a test failed midway through, resources (coupons, promo codes, subscriptions, customers) would accumulate in Stripe test mode.
### Example Issue
Running `npm run test:promo` would create:
- Dozens of test coupons
- Multiple promo codes
- Test customers and subscriptions
- Database records
If interrupted or failed, these resources remained in Stripe, eventually hitting rate limits or causing test conflicts.
## Root Cause
The batch conversion script wrapped entire test files in a single `it()` block without extracting cleanup logic into proper Mocha hooks. This meant:
```javascript
// ❌ BEFORE - Cleanup inside test logic
describe('Test Name', function() {
it('should execute test successfully', async function() {
const resources = [];
try {
// Create resources
resources.push(await createResource());
// Test logic
// If this fails, cleanup never runs...
} finally {
// Cleanup (only runs if try block completes or throws)
for (const resource of resources) {
await deleteResource(resource);
}
}
});
});
```
**Problem**: If Node process crashes, test times out, or `finally` block has errors, cleanup doesn't complete.
## Solution
Refactored tests to use Mocha's `before()` and `after()` hooks which ALWAYS run, even on test failure:
```javascript
// ✅ AFTER - Cleanup in proper hooks
describe('Test Name', function() {
const resources = []; // Outer scope
after(async function() {
// Always runs, even if test fails
for (const resource of resources) {
await deleteResource(resource);
}
});
it('should execute test successfully', async function() {
// Create resources
resources.push(await createResource());
// Test logic
// If this fails, after() hook still runs
});
});
```
## Fixed Files
### 1. test_promo_details.js
**Before**: Created 7 subscriptions, 6 coupons, 6 promo codes, 1 customer, all cleaned up in `finally` block
**After**:
- Moved `createdResources` tracking to outer scope
- Added `after()` hook that always runs
- Cleanup deactivates promo codes, deletes coupons, deletes customers
**Test Output**:
```
✔ should execute test successfully (30022ms)
🧹 Cleaning up test resources...
✅ Deactivated promo: promo_1SxwgGJxyI1MWs2TgzT7m5sW
✅ Deactivated promo: promo_1SxwgKJxyI1MWs2TpU1rlV4f
✅ Deactivated promo: promo_1SxwgOJxyI1MWs2TTwjmv4jB
✅ Deactivated promo: promo_1SxwgWJxyI1MWs2TVDnhaUEK
✅ Deactivated promo: promo_1SxwgaJxyI1MWs2ThNgnIAmm
✅ Deleted coupon: BdctKo7T
✅ Deleted coupon: ohFzcBtw
✅ Deleted coupon: R50fQNMX
✅ Deleted coupon: 7nKGLfip
✅ Deleted coupon: RpqBB216
✅ Deleted coupon: Ijs0XjPk
✅ Deleted customer: cus_TvoI9Uk7Lq5KRc
✅ Cleanup complete!
```
### 2. test_forever_coupon_validation.js
**Before**: Created 3 test coupons, modified database settings, cleanup in function called from `finally` block
**After**:
- Added `before()` hook to connect to DB and run initial cleanup
- Added `after()` hook to cleanup and disconnect from DB
- Ensures database connection properly managed
**Test Output**:
```
✔ should execute test successfully
🧹 Cleanup ===
✅ Deleted coupon: TEST_FOREVER_50
✅ Deleted coupon: TEST_ONCE_50
✅ Deleted coupon: TEST_REPEAT_50
✅ Removed 0 test promo(s) from settings
```
### 3. test_coupon_resolution.js
**Before**: Created multiple coupons/promos per test case, inline cleanup after each case
**After**:
- Added resource tracking array
- Added `after()` hook as safety net
- Inline cleanup still runs (good practice)
- Hook catches anything missed if test fails
**Test Output**:
```
✔ should execute test successfully (2503ms)
🧹 Cleanup hook - cleaning up any remaining resources...
✅ Deactivated promo: promo_1SxwfkJxyI1MWs2TL1RsVwKJ
✅ Deactivated promo: promo_1SxwflJxyI1MWs2TxMieyuPc
❌ Failed to delete coupon 3937ASxh: No such coupon (already cleaned up inline ✓)
```
## Key Improvements
### 1. **Resource Tracking**
```javascript
const createdResources = {
customers: [],
subscriptions: [],
promoCodes: [],
coupons: []
};
// Track when creating
const coupon = await stripe.coupons.create({...});
createdResources.coupons.push(coupon.id);
```
### 2. **Guaranteed Cleanup**
```javascript
after(async function() {
this.timeout(60000); // 1 minute for cleanup
for (const id of createdResources.promoCodes) {
await stripe.promotionCodes.update(id, { active: false });
}
for (const id of createdResources.coupons) {
await stripe.coupons.del(id);
}
});
```
### 3. **Rate Limiting**
```javascript
// 100ms delay between Stripe API calls (10 ops/sec safe)
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
for (const id of resources) {
await deleteResource(id);
await sleep(100); // Prevent hitting Stripe's 25 ops/sec limit
}
```
### 4. **Unique Naming**
```javascript
// Avoid conflicts with previous test runs
const TEST_RUN_ID = Date.now();
const coupon = await stripe.coupons.create({
name: `Test Coupon ${TEST_RUN_ID}`
});
```
## Testing Verification
```bash
# Test individual fixed file
npm run test:single tests/promo/test_promo_details.js
# Test all promo tests
npm run test:promo
# Verify cleanup runs
# Look for these lines in output:
# ✅ Deactivated promo: ...
# ✅ Deleted coupon: ...
# ✅ Deleted customer: ...
# ✅ Cleanup complete!
```
## Benefits
1. **No Resource Leaks**: Cleanup always runs, even on test failure
2. **Faster Test Runs**: No accumulation of test data between runs
3. **No Rate Limit Issues**: Fewer resources = fewer API calls
4. **Predictable State**: Each test starts clean
5. **Better Debugging**: Clear cleanup logs show what was created/deleted
## Stripe Rate Limiting Best Practices
Following guidelines from `.github/copilot-instructions.md`:
### ❌ Bad Practices (Avoided)
- Disabling/fetching 100+ existing records in tests
- Cleanup all records before each test case
- Using `.limit()` queries that might miss data
### ✅ Good Practices (Implemented)
- Use unique names (timestamps) to avoid conflicts
- Track only what you create and cleanup only those
- Use 100ms+ delays between API calls (10 ops/sec safe)
- Use Stripe SDK's async iteration for auto-pagination
- Cleanup in `after()` hooks that always run
## Pattern for Future Tests
When creating new tests that use Stripe or other external resources:
```javascript
describe('My Test Suite', function() {
this.timeout(120000);
// Setup
const TEST_RUN_ID = Date.now();
const createdResources = {
resources: []
};
// Cleanup hook - ALWAYS runs
after(async function() {
this.timeout(60000);
console.log('\n🧹 Cleaning up...');
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
for (const id of createdResources.resources) {
try {
await api.delete(id);
console.log(`✅ Deleted: ${id}`);
await sleep(100); // Rate limiting
} catch (err) {
console.error(`❌ Failed: ${id}`, err.message);
}
}
});
it('should do something', async function() {
// Create resource
const resource = await api.create({
name: `Test_${TEST_RUN_ID}`
});
createdResources.resources.push(resource.id);
// Test logic
expect(resource).to.exist;
// No cleanup here - after() hook handles it
});
});
```
## Summary
**Problem**: Promo tests created Stripe resources without guaranteed cleanup
**Solution**: Added Mocha `after()` hooks to ensure cleanup always runs
**Fixed**: 3 promo test files properly refactored
**Verified**: Cleanup works even on test failure
**Best Practices**: Rate limiting, unique naming, resource tracking
All Mocha tests now follow proper cleanup patterns and won't leak resources!