12 KiB
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:
npm run test:unit -- sanitize
Hook Tests (Future)
Test custom hooks in isolation:
useModerationQueueuseModerationActionsuseQueueQuery
Example:
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:
-
Role Validation
- Regular users cannot perform moderation actions
- Only moderators/admins/superusers can validate actions
-
Lock Enforcement
- Cannot modify submission locked by another moderator
- Lock must be claimed before approve/reject
- Expired locks are automatically released
-
Audit Logging
- All actions logged in
moderation_audit_log - Logs include metadata (notes, status changes)
- Logs are immutable (cannot be modified)
- All actions logged in
-
Rate Limiting
- Maximum 10 actions per minute per user
- 11th action within minute fails with rate limit error
Run:
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_logto verify cleanup - Check
getTestDataStats()for remaining records
Example:
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:
-
Claim Submission
- Click "Claim Submission" button
- Verify lock badge appears ("Claimed by you")
- Verify approve/reject buttons enabled
-
Lock Timer
- Verify countdown displays (14:XX format)
- Verify lock status badge visible
-
Extend Lock
- Wait for timer to reach < 5 minutes
- Verify "Extend Lock" button appears
- Click extend, verify timer resets
-
Release Lock
- Click "Release Lock" button
- Verify "Claim Submission" button reappears
- Verify approve/reject buttons disabled
-
Locked by Another
- Verify lock badge for items locked by others
- Verify actions disabled
Run:
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:
// 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):
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 rolecleanupTestData()- Remove all test dataqueryDatabase()- Direct database queries for assertionswaitForVersion()- Wait for version record to be createdapproveSubmissionDirect()- Bypass UI for test setupgetTestDataStats()- Get count of test records
Example:
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 rolesgetTestUserCredentials()- Get email/password for roleloginAsUser()- Programmatic loginlogout()- Programmatic logout
Test Users:
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
npm run test
Unit Tests Only
npm run test:unit
Integration Tests Only
npm run test:integration
E2E Tests Only
npm run test:e2e
Specific Test File
npm run test:e2e -- lock-management
npm run test:integration -- moderation-security
npm run test:unit -- sanitize
Watch Mode
npm run test:watch
Coverage Report
npm run test:coverage
Writing New Tests
Unit Test Template
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
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
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
afterEachorafterAll - ✅ 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
test123everywhere
3. Error Handling
Test both success and failure cases:
// 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:
// ❌ 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:
// ❌ Vague
test('test 1', () => { ... });
// ✅ Clear
test('should prevent non-moderator from approving submission', () => { ... });
Debugging Tests
Enable Debug Mode
# 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
// In tests
console.log('Debug info:', variable);
// View logs
npm run test -- --verbose
Screenshots on Failure
// In playwright.config.ts
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
Database Inspection
// 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)
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:
npm run test:coverage
open coverage/index.html
Troubleshooting
Test Timeout
// 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
waitFororwaitForSelector - Network delays: Increase timeout, add retries
- Test data conflicts: Ensure unique IDs
Database Connection Issues
// 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)