mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:31:12 -05:00
Approve tool use
This commit is contained in:
566
docs/moderation/TESTING.md
Normal file
566
docs/moderation/TESTING.md
Normal file
@@ -0,0 +1,566 @@
|
||||
# Moderation Queue Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive testing strategy for the moderation queue component covering unit tests, integration tests, and end-to-end tests.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Fast, isolated tests
|
||||
│ └── sanitize.test.ts # Input sanitization
|
||||
├── integration/ # Database + API tests
|
||||
│ └── moderation-security.test.ts
|
||||
├── e2e/ # Browser-based tests
|
||||
│ └── moderation/
|
||||
│ └── lock-management.spec.ts
|
||||
├── fixtures/ # Shared test utilities
|
||||
│ ├── auth.ts # Authentication helpers
|
||||
│ └── database.ts # Database setup/teardown
|
||||
└── setup/
|
||||
├── global-setup.ts # Runs before all tests
|
||||
└── global-teardown.ts # Runs after all tests
|
||||
```
|
||||
|
||||
## Unit Tests
|
||||
|
||||
### Sanitization Tests
|
||||
|
||||
**File:** `tests/unit/sanitize.test.ts`
|
||||
|
||||
Tests XSS protection utilities:
|
||||
- URL validation (block `javascript:`, `data:` protocols)
|
||||
- HTML entity escaping
|
||||
- Plain text sanitization
|
||||
- Suspicious content detection
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
npm run test:unit -- sanitize
|
||||
```
|
||||
|
||||
### Hook Tests (Future)
|
||||
|
||||
Test custom hooks in isolation:
|
||||
- `useModerationQueue`
|
||||
- `useModerationActions`
|
||||
- `useQueueQuery`
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useModerationQueue } from '@/hooks/useModerationQueue';
|
||||
|
||||
test('should claim submission', async () => {
|
||||
const { result } = renderHook(() => useModerationQueue());
|
||||
|
||||
const success = await result.current.claimSubmission('test-id');
|
||||
expect(success).toBe(true);
|
||||
expect(result.current.currentLock).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### Moderation Security Tests
|
||||
|
||||
**File:** `tests/integration/moderation-security.test.ts`
|
||||
|
||||
Tests backend security enforcement:
|
||||
|
||||
1. **Role Validation**
|
||||
- Regular users cannot perform moderation actions
|
||||
- Only moderators/admins/superusers can validate actions
|
||||
|
||||
2. **Lock Enforcement**
|
||||
- Cannot modify submission locked by another moderator
|
||||
- Lock must be claimed before approve/reject
|
||||
- Expired locks are automatically released
|
||||
|
||||
3. **Audit Logging**
|
||||
- All actions logged in `moderation_audit_log`
|
||||
- Logs include metadata (notes, status changes)
|
||||
- Logs are immutable (cannot be modified)
|
||||
|
||||
4. **Rate Limiting**
|
||||
- Maximum 10 actions per minute per user
|
||||
- 11th action within minute fails with rate limit error
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
npm run test:integration -- moderation-security
|
||||
```
|
||||
|
||||
### Test Data Management
|
||||
|
||||
**Setup:**
|
||||
- Uses service role key to create test users and data
|
||||
- All test data marked with `is_test_data: true`
|
||||
- Isolated from production data
|
||||
|
||||
**Cleanup:**
|
||||
- Global teardown removes all test data
|
||||
- Query `moderation_audit_log` to verify cleanup
|
||||
- Check `getTestDataStats()` for remaining records
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import { setupTestUser, cleanupTestData } from '../fixtures/database';
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await cleanupTestData();
|
||||
await setupTestUser('test@example.com', 'password', 'moderator');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await cleanupTestData();
|
||||
});
|
||||
```
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
### Lock Management E2E
|
||||
|
||||
**File:** `tests/e2e/moderation/lock-management.spec.ts`
|
||||
|
||||
Browser-based tests using Playwright:
|
||||
|
||||
1. **Claim Submission**
|
||||
- Click "Claim Submission" button
|
||||
- Verify lock badge appears ("Claimed by you")
|
||||
- Verify approve/reject buttons enabled
|
||||
|
||||
2. **Lock Timer**
|
||||
- Verify countdown displays (14:XX format)
|
||||
- Verify lock status badge visible
|
||||
|
||||
3. **Extend Lock**
|
||||
- Wait for timer to reach < 5 minutes
|
||||
- Verify "Extend Lock" button appears
|
||||
- Click extend, verify timer resets
|
||||
|
||||
4. **Release Lock**
|
||||
- Click "Release Lock" button
|
||||
- Verify "Claim Submission" button reappears
|
||||
- Verify approve/reject buttons disabled
|
||||
|
||||
5. **Locked by Another**
|
||||
- Verify lock badge for items locked by others
|
||||
- Verify actions disabled
|
||||
|
||||
**Run:**
|
||||
```bash
|
||||
npm run test:e2e -- lock-management
|
||||
```
|
||||
|
||||
### Authentication in E2E Tests
|
||||
|
||||
**Global Setup** (`tests/setup/global-setup.ts`):
|
||||
- Creates test users for all roles (user, moderator, admin, superuser)
|
||||
- Logs in each user and saves auth state to `.auth/` directory
|
||||
- Auth states reused across all tests (faster execution)
|
||||
|
||||
**Test Usage:**
|
||||
```typescript
|
||||
// Use saved auth state
|
||||
test.use({ storageState: '.auth/moderator.json' });
|
||||
|
||||
test('moderator can access queue', async ({ page }) => {
|
||||
await page.goto('/moderation/queue');
|
||||
// Already authenticated as moderator
|
||||
});
|
||||
```
|
||||
|
||||
**Manual Login (if needed):**
|
||||
```typescript
|
||||
import { loginAsUser } from '../fixtures/auth';
|
||||
|
||||
const { userId, accessToken } = await loginAsUser(
|
||||
'test@example.com',
|
||||
'password'
|
||||
);
|
||||
```
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
### Database Fixtures
|
||||
|
||||
**File:** `tests/fixtures/database.ts`
|
||||
|
||||
**Functions:**
|
||||
- `setupTestUser()` - Create test user with specific role
|
||||
- `cleanupTestData()` - Remove all test data
|
||||
- `queryDatabase()` - Direct database queries for assertions
|
||||
- `waitForVersion()` - Wait for version record to be created
|
||||
- `approveSubmissionDirect()` - Bypass UI for test setup
|
||||
- `getTestDataStats()` - Get count of test records
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import { setupTestUser, supabaseAdmin } from '../fixtures/database';
|
||||
|
||||
// Create moderator
|
||||
const { userId } = await setupTestUser(
|
||||
'mod@test.com',
|
||||
'password',
|
||||
'moderator'
|
||||
);
|
||||
|
||||
// Create test submission
|
||||
const { data } = await supabaseAdmin
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
submission_type: 'review',
|
||||
status: 'pending',
|
||||
submitted_by: userId,
|
||||
is_test_data: true,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
```
|
||||
|
||||
### Auth Fixtures
|
||||
|
||||
**File:** `tests/fixtures/auth.ts`
|
||||
|
||||
**Functions:**
|
||||
- `setupAuthStates()` - Create auth states for all roles
|
||||
- `getTestUserCredentials()` - Get email/password for role
|
||||
- `loginAsUser()` - Programmatic login
|
||||
- `logout()` - Programmatic logout
|
||||
|
||||
**Test Users:**
|
||||
```typescript
|
||||
const TEST_USERS = {
|
||||
user: 'test-user@thrillwiki.test',
|
||||
moderator: 'test-moderator@thrillwiki.test',
|
||||
admin: 'test-admin@thrillwiki.test',
|
||||
superuser: 'test-superuser@thrillwiki.test',
|
||||
};
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### All Tests
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Unit Tests Only
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Integration Tests Only
|
||||
```bash
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
### E2E Tests Only
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Specific Test File
|
||||
```bash
|
||||
npm run test:e2e -- lock-management
|
||||
npm run test:integration -- moderation-security
|
||||
npm run test:unit -- sanitize
|
||||
```
|
||||
|
||||
### Watch Mode
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Unit Test Template
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from '@playwright/test';
|
||||
import { functionToTest } from '@/lib/module';
|
||||
|
||||
describe('functionToTest', () => {
|
||||
it('should handle valid input', () => {
|
||||
const result = functionToTest('valid input');
|
||||
expect(result).toBe('expected output');
|
||||
});
|
||||
|
||||
it('should handle edge case', () => {
|
||||
const result = functionToTest('');
|
||||
expect(result).toBe('default value');
|
||||
});
|
||||
|
||||
it('should throw on invalid input', () => {
|
||||
expect(() => functionToTest(null)).toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Test Template
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { setupTestUser, supabaseAdmin, cleanupTestData } from '../fixtures/database';
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
test.beforeAll(async () => {
|
||||
await cleanupTestData();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await cleanupTestData();
|
||||
});
|
||||
|
||||
test('should perform action', async () => {
|
||||
// Setup
|
||||
const { userId } = await setupTestUser(
|
||||
'test@example.com',
|
||||
'password',
|
||||
'moderator'
|
||||
);
|
||||
|
||||
// Action
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('table_name')
|
||||
.insert({ ... });
|
||||
|
||||
// Assert
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Test Template
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.use({ storageState: '.auth/moderator.json' });
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/moderation/queue');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should interact with UI', async ({ page }) => {
|
||||
// Find element
|
||||
const button = page.locator('button:has-text("Action")');
|
||||
|
||||
// Assert initial state
|
||||
await expect(button).toBeVisible();
|
||||
await expect(button).toBeEnabled();
|
||||
|
||||
// Perform action
|
||||
await button.click();
|
||||
|
||||
// Assert result
|
||||
await expect(page.locator('text=Success')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Test Isolation
|
||||
|
||||
Each test should be independent:
|
||||
- ✅ Clean up test data in `afterEach` or `afterAll`
|
||||
- ✅ Use unique identifiers for test records
|
||||
- ❌ Don't rely on data from previous tests
|
||||
|
||||
### 2. Realistic Test Data
|
||||
|
||||
Use realistic data patterns:
|
||||
- ✅ Valid email formats
|
||||
- ✅ Appropriate string lengths
|
||||
- ✅ Realistic timestamps
|
||||
- ❌ Don't use `test123` everywhere
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
Test both success and failure cases:
|
||||
```typescript
|
||||
// Test success
|
||||
test('should approve valid submission', async () => {
|
||||
const { error } = await approveSubmission(validId);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
// Test failure
|
||||
test('should reject invalid submission', async () => {
|
||||
const { error } = await approveSubmission(invalidId);
|
||||
expect(error).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Async Handling
|
||||
|
||||
Always await async operations:
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
test('test name', () => {
|
||||
asyncFunction(); // Not awaited
|
||||
expect(result).toBe(value); // May run before async completes
|
||||
});
|
||||
|
||||
// ✅ CORRECT
|
||||
test('test name', async () => {
|
||||
await asyncFunction();
|
||||
expect(result).toBe(value);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Descriptive Test Names
|
||||
|
||||
Use clear, descriptive names:
|
||||
```typescript
|
||||
// ❌ Vague
|
||||
test('test 1', () => { ... });
|
||||
|
||||
// ✅ Clear
|
||||
test('should prevent non-moderator from approving submission', () => { ... });
|
||||
```
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
```bash
|
||||
# Playwright debug mode (E2E)
|
||||
PWDEBUG=1 npm run test:e2e -- lock-management
|
||||
|
||||
# Show browser during E2E tests
|
||||
npm run test:e2e -- --headed
|
||||
|
||||
# Slow down actions for visibility
|
||||
npm run test:e2e -- --slow-mo=1000
|
||||
```
|
||||
|
||||
### Console Logging
|
||||
|
||||
```typescript
|
||||
// In tests
|
||||
console.log('Debug info:', variable);
|
||||
|
||||
// View logs
|
||||
npm run test -- --verbose
|
||||
```
|
||||
|
||||
### Screenshots on Failure
|
||||
|
||||
```typescript
|
||||
// In playwright.config.ts
|
||||
use: {
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
}
|
||||
```
|
||||
|
||||
### Database Inspection
|
||||
|
||||
```typescript
|
||||
// Query database during test
|
||||
const { data } = await supabaseAdmin
|
||||
.from('content_submissions')
|
||||
.select('*')
|
||||
.eq('id', testId);
|
||||
|
||||
console.log('Submission state:', data);
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions (Example)
|
||||
|
||||
```yaml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration
|
||||
env:
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
BASE_URL: http://localhost:8080
|
||||
```
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
- **Unit Tests:** 90%+ coverage
|
||||
- **Integration Tests:** All critical paths covered
|
||||
- **E2E Tests:** Happy paths + key error scenarios
|
||||
|
||||
**Generate Coverage Report:**
|
||||
```bash
|
||||
npm run test:coverage
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Test Timeout
|
||||
|
||||
```typescript
|
||||
// Increase timeout for slow operations
|
||||
test('slow test', async () => {
|
||||
test.setTimeout(60000); // 60 seconds
|
||||
await slowOperation();
|
||||
});
|
||||
```
|
||||
|
||||
### Flaky Tests
|
||||
|
||||
Common causes and fixes:
|
||||
- **Race conditions:** Add `waitFor` or `waitForSelector`
|
||||
- **Network delays:** Increase timeout, add retries
|
||||
- **Test data conflicts:** Ensure unique IDs
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
```typescript
|
||||
// Check connection
|
||||
if (!supabaseAdmin) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
```
|
||||
|
||||
## Future Test Coverage
|
||||
|
||||
- [ ] Unit tests for all custom hooks
|
||||
- [ ] Component snapshot tests
|
||||
- [ ] Accessibility tests (axe-core)
|
||||
- [ ] Performance tests (lighthouse)
|
||||
- [ ] Load testing (k6 or similar)
|
||||
- [ ] Visual regression tests (Percy/Chromatic)
|
||||
Reference in New Issue
Block a user