← 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;
  },
};