← back to Wine Finder Next
__tests__/helpers/testUtils.ts
374 lines
// Test Helper Utilities
import { NextRequest } from 'next/server';
/**
* Create a mock Next.js request
*/
export function createMockRequest(
url: string,
options: {
method?: string;
body?: any;
headers?: Record<string, string>;
} = {}
): NextRequest {
const { method = 'GET', body, headers = {} } = options;
const requestInit: any = {
method,
headers: new Headers(headers),
};
if (body) {
requestInit.body = JSON.stringify(body);
}
return new NextRequest(url, requestInit);
}
/**
* Wait for a condition to be true
*/
export async function waitFor(
condition: () => boolean | Promise<boolean>,
timeout: number = 5000,
interval: number = 100
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await condition()) {
return;
}
await sleep(interval);
}
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
}
/**
* Sleep for specified milliseconds
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Create a spy object with methods
*/
export function createSpy<T extends Record<string, any>>(): T {
return new Proxy({} as T, {
get: (target, prop) => {
if (!target[prop as keyof T]) {
target[prop as keyof T] = jest.fn() as any;
}
return target[prop as keyof T];
},
});
}
/**
* Mock timer utilities
*/
export function mockTimers() {
jest.useFakeTimers();
return {
advanceBy: (ms: number) => jest.advanceTimersByTime(ms),
runAll: () => jest.runAllTimers(),
runPending: () => jest.runOnlyPendingTimers(),
restore: () => jest.useRealTimers(),
};
}
/**
* Generate random test data
*/
export const generateRandom = {
email: () => `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}@example.com`,
userId: () => `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
bottleId: () => `bottle_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
price: (min = 1, max = 1000) => Math.random() * (max - min) + min,
string: (length = 10) => Math.random().toString(36).substr(2, length),
};
/**
* Assert API response structure
*/
export function assertApiResponse(
response: any,
expected: {
status?: number;
success?: boolean;
hasData?: boolean;
hasError?: boolean;
}
) {
if (expected.status) {
expect(response.status).toBe(expected.status);
}
if (expected.success !== undefined) {
expect(response.success).toBe(expected.success);
}
if (expected.hasData) {
expect(response.data).toBeDefined();
}
if (expected.hasError) {
expect(response.error).toBeDefined();
}
}
/**
* Performance measurement utility
*/
export class PerformanceMonitor {
private startTime: number = 0;
private measurements: Map<string, number[]> = new Map();
start() {
this.startTime = performance.now();
}
end(label: string) {
const duration = performance.now() - this.startTime;
if (!this.measurements.has(label)) {
this.measurements.set(label, []);
}
this.measurements.get(label)!.push(duration);
return duration;
}
getAverage(label: string): number {
const measures = this.measurements.get(label);
if (!measures || measures.length === 0) return 0;
return measures.reduce((a, b) => a + b, 0) / measures.length;
}
getStats(label: string) {
const measures = this.measurements.get(label);
if (!measures || measures.length === 0) {
return { min: 0, max: 0, avg: 0, count: 0 };
}
return {
min: Math.min(...measures),
max: Math.max(...measures),
avg: this.getAverage(label),
count: measures.length,
};
}
reset() {
this.measurements.clear();
}
}
/**
* Database test helpers
*/
export class DatabaseTestHelper {
static async cleanDatabase() {
// Reset in-memory database
const { bottleDb, membershipUnitDb, governanceVoteDb, marketplaceDb } =
require('@/lib/membershipDatabase');
// Note: This would need actual implementation based on database structure
// For now, it's a placeholder
}
static createBottle(overrides = {}) {
const { bottleDb } = require('@/lib/membershipDatabase');
return bottleDb.create({
name: 'Test Wine',
vintage: 2010,
producer: 'Test Producer',
region: 'Test Region',
size: '750ml',
totalMembershipUnits: 1000,
availableMembershipUnits: 1000,
estimatedValue: 50000,
provenance: 'Test provenance',
status: 'active',
...overrides,
});
}
static createMembershipUnit(bottleId: string, overrides = {}) {
const { membershipUnitDb } = require('@/lib/membershipDatabase');
return membershipUnitDb.create({
bottleId,
tokenId: `TOKEN_${Date.now()}`,
ownerId: 'user_1',
ownerEmail: 'test@example.com',
ownerName: 'Test User',
purchasePrice: 10,
purchaseDate: new Date(),
status: 'active',
votingPower: 1,
transactionHistory: [],
...overrides,
});
}
}
/**
* Mock fetch responses
*/
export function mockFetch(responses: Map<string, any>) {
global.fetch = jest.fn((url: string) => {
const response = responses.get(url);
if (!response) {
return Promise.reject(new Error(`No mock for URL: ${url}`));
}
return Promise.resolve({
ok: response.status >= 200 && response.status < 300,
status: response.status || 200,
json: async () => response.data,
text: async () => JSON.stringify(response.data),
headers: new Headers(response.headers || {}),
} as Response);
});
}
/**
* Restore mocks
*/
export function restoreMocks() {
jest.restoreAllMocks();
jest.clearAllMocks();
}
/**
* Test rate limiting
*/
export async function testRateLimit(
endpoint: string,
limit: number,
windowMs: number
): Promise<{ withinLimit: number; overLimit: number }> {
const results = {
withinLimit: 0,
overLimit: 0,
};
for (let i = 0; i < limit + 5; i++) {
const response = await fetch(endpoint);
if (response.status === 200) {
results.withinLimit++;
} else if (response.status === 429) {
results.overLimit++;
}
}
return results;
}
/**
* Security test utilities
*/
export const securityTestUtils = {
sqlInjectionPayloads: [
"'; DROP TABLE users; --",
"1' OR '1'='1",
"admin'--",
"' UNION SELECT * FROM passwords--",
],
xssPayloads: [
'<script>alert("XSS")</script>',
'<img src=x onerror=alert(1)>',
'javascript:alert(document.cookie)',
'<iframe src="javascript:alert(1)">',
],
testPayload: async (endpoint: string, payload: string, method = 'POST') => {
const response = await fetch(endpoint, {
method,
body: JSON.stringify({ test: payload }),
headers: { 'Content-Type': 'application/json' },
});
return {
blocked: response.status === 403 || response.status === 400,
status: response.status,
};
},
};
/**
* Async test utilities
*/
export const asyncTestUtils = {
retry: async <T>(
fn: () => Promise<T>,
retries = 3,
delay = 1000
): Promise<T> => {
try {
return await fn();
} catch (error) {
if (retries === 0) throw error;
await sleep(delay);
return asyncTestUtils.retry(fn, retries - 1, delay);
}
},
timeout: async <T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> => {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
),
]);
},
};
/**
* Console spy for testing logs
*/
export function spyConsole() {
const originalConsole = { ...console };
const logs: string[] = [];
const errors: string[] = [];
const warns: string[] = [];
console.log = jest.fn((...args) => logs.push(args.join(' ')));
console.error = jest.fn((...args) => errors.push(args.join(' ')));
console.warn = jest.fn((...args) => warns.push(args.join(' ')));
return {
logs: () => logs,
errors: () => errors,
warns: () => warns,
restore: () => {
console.log = originalConsole.log;
console.error = originalConsole.error;
console.warn = originalConsole.warn;
},
};
}
/**
* Date utilities for testing
*/
export const dateUtils = {
now: () => new Date('2024-01-15T00:00:00Z'),
tomorrow: () => new Date('2024-01-16T00:00:00Z'),
yesterday: () => new Date('2024-01-14T00:00:00Z'),
addDays: (date: Date, days: number) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
},
};