← back to Handbag Auth Nextjs

auction-viewer/tests/performance/load.test.js

380 lines

/**
 * Performance Tests - Load Testing and Benchmarks
 */

const request = require('supertest');
const express = require('express');
const Database = require('better-sqlite3');
const { TestDatabase } = require('../fixtures/test-database');
const { generateLargeDataset } = require('../fixtures/test-data');

describe('Performance Tests', () => {
  let app;
  let testDb;
  let testDbPath;

  beforeAll(() => {
    // Create large test database with unique path for performance tests
    const path = require('path');
    const uniquePath = path.join(__dirname, '../fixtures', `perf-test-${Date.now()}.db`);
    testDb = new TestDatabase(uniquePath);
    testDb.initialize();
    testDb.seed(generateLargeDataset(2000));
    testDbPath = testDb.getPath();

    // Create Express app
    app = express();
    app.use(express.json());

    app.get('/api/auctions', (req, res) => {
      try {
        const db = new Database(testDbPath, { readonly: true });
        const auctions = db.prepare('SELECT * FROM auctions ORDER BY created_at DESC LIMIT 1000').all();
        db.close();
        res.json({ success: true, count: auctions.length, data: auctions });
      } catch (error) {
        res.json({ success: false, error: error.message, data: [] });
      }
    });

    app.get('/api/best-values', (req, res) => {
      try {
        const db = new Database(testDbPath, { readonly: true });
        const sortBy = req.query.sort || 'percent';
        const orderBy = sortBy === 'dollar' ? 'savings_amount' : 'savings_percent';

        const auctions = db.prepare(`
          SELECT *,
            CASE
              WHEN estimate_low > 0 AND current_price > 0
              THEN ((estimate_low - current_price) / estimate_low * 100)
              ELSE 0
            END as savings_percent,
            CASE
              WHEN estimate_low > 0 AND current_price > 0
              THEN (estimate_low - current_price)
              ELSE 0
            END as savings_amount
          FROM auctions
          WHERE current_price > 0 AND estimate_low > 0
          ORDER BY ${orderBy} DESC
          LIMIT 100
        `).all();

        db.close();
        res.json({ success: true, count: auctions.length, data: auctions, sortedBy: sortBy });
      } catch (error) {
        res.json({ success: false, error: error.message, data: [] });
      }
    });

    app.get('/api/stats', (req, res) => {
      try {
        const db = new Database(testDbPath, { readonly: true });

        const total = db.prepare('SELECT COUNT(*) as count FROM auctions').get();
        const byBrand = db.prepare(`
          SELECT search_term, COUNT(*) as count
          FROM auctions
          GROUP BY search_term
          ORDER BY count DESC
        `).all();

        const byAuctionHouse = db.prepare(`
          SELECT auction_house, COUNT(*) as count
          FROM auctions
          GROUP BY auction_house
          ORDER BY count DESC
        `).all();

        const avgPrice = db.prepare(`
          SELECT AVG(current_price) as avg_price
          FROM auctions
          WHERE current_price > 0
        `).get();

        db.close();

        res.json({
          success: true,
          stats: {
            total: total.count,
            byBrand,
            byAuctionHouse,
            avgPrice: avgPrice.avg_price || 0
          }
        });
      } catch (error) {
        res.json({ success: false, error: error.message });
      }
    });
  });

  afterAll(() => {
    testDb.cleanup();
  });

  describe('Response Time Benchmarks', () => {
    test('GET /api/auctions should respond within 200ms', async () => {
      const start = Date.now();
      const response = await request(app).get('/api/auctions');
      const duration = Date.now() - start;

      expect(response.status).toBe(200);
      expect(duration).toBeLessThan(200);
    }, 10000);

    test('GET /api/best-values should respond within 300ms', async () => {
      const start = Date.now();
      const response = await request(app).get('/api/best-values');
      const duration = Date.now() - start;

      expect(response.status).toBe(200);
      expect(duration).toBeLessThan(300);
    }, 10000);

    test('GET /api/stats should respond within 250ms', async () => {
      const start = Date.now();
      const response = await request(app).get('/api/stats');
      const duration = Date.now() - start;

      expect(response.status).toBe(200);
      expect(duration).toBeLessThan(250);
    }, 10000);
  });

  describe('Concurrent Request Handling', () => {
    test('should handle 10 concurrent requests', async () => {
      const requests = Array.from({ length: 10 }, () =>
        request(app).get('/api/auctions')
      );

      const start = Date.now();
      const responses = await Promise.all(requests);
      const duration = Date.now() - start;

      responses.forEach(response => {
        expect(response.status).toBe(200);
        expect(response.body.success).toBe(true);
      });

      // Should complete within 2 seconds for 10 concurrent requests
      expect(duration).toBeLessThan(2000);
    }, 15000);

    test('should handle 50 concurrent requests', async () => {
      const requests = Array.from({ length: 50 }, () =>
        request(app).get('/api/best-values')
      );

      const start = Date.now();
      const responses = await Promise.all(requests);
      const duration = Date.now() - start;

      responses.forEach(response => {
        expect(response.status).toBe(200);
        expect(response.body.success).toBe(true);
      });

      // Should complete within 5 seconds for 50 concurrent requests
      expect(duration).toBeLessThan(5000);
    }, 20000);

    test('should handle mixed endpoint concurrent requests', async () => {
      const requests = [
        ...Array.from({ length: 10 }, () => request(app).get('/api/auctions')),
        ...Array.from({ length: 10 }, () => request(app).get('/api/best-values')),
        ...Array.from({ length: 10 }, () => request(app).get('/api/stats'))
      ];

      const start = Date.now();
      const responses = await Promise.all(requests);
      const duration = Date.now() - start;

      responses.forEach(response => {
        expect(response.status).toBe(200);
        expect(response.body.success).toBe(true);
      });

      expect(duration).toBeLessThan(3000);
    }, 15000);
  });

  describe('Large Dataset Performance', () => {
    test('should handle queries on 2000+ records efficiently', async () => {
      const response = await request(app).get('/api/auctions');

      expect(response.status).toBe(200);
      expect(response.body.data.length).toBeGreaterThan(0);
      expect(response.body.data.length).toBeLessThanOrEqual(1000); // Respects limit
    });

    test('should perform complex sorting on large dataset', async () => {
      const start = Date.now();
      const response = await request(app).get('/api/best-values?sort=percent');
      const duration = Date.now() - start;

      expect(response.status).toBe(200);
      expect(duration).toBeLessThan(400);

      // Verify sorting
      const data = response.body.data;
      for (let i = 0; i < data.length - 1; i++) {
        expect(data[i].savings_percent).toBeGreaterThanOrEqual(data[i + 1].savings_percent);
      }
    });

    test('should perform aggregations on large dataset', async () => {
      const start = Date.now();
      const response = await request(app).get('/api/stats');
      const duration = Date.now() - start;

      expect(response.status).toBe(200);
      expect(duration).toBeLessThan(300);
      expect(response.body.stats.total).toBeGreaterThanOrEqual(2000);
    });
  });

  describe('Database Query Performance', () => {
    test('should execute SELECT queries efficiently', () => {
      const db = new Database(testDbPath, { readonly: true });

      const iterations = 100;
      const start = Date.now();

      for (let i = 0; i < iterations; i++) {
        db.prepare('SELECT * FROM auctions LIMIT 100').all();
      }

      const duration = Date.now() - start;
      db.close();

      // 100 queries should complete in less than 1 second
      expect(duration).toBeLessThan(1000);

      const avgTime = duration / iterations;
      expect(avgTime).toBeLessThan(10); // Average < 10ms per query
    });

    test('should execute complex queries with calculations efficiently', () => {
      const db = new Database(testDbPath, { readonly: true });

      const start = Date.now();
      const result = db.prepare(`
        SELECT *,
          ((estimate_low - current_price) / estimate_low * 100) as savings_percent,
          (estimate_low - current_price) as savings_amount
        FROM auctions
        WHERE current_price > 0 AND estimate_low > 0
        ORDER BY savings_percent DESC
        LIMIT 100
      `).all();
      const duration = Date.now() - start;

      db.close();

      expect(result.length).toBeGreaterThan(0);
      expect(duration).toBeLessThan(100); // Should be very fast
    });

    test('should execute aggregation queries efficiently', () => {
      const db = new Database(testDbPath, { readonly: true });

      const start = Date.now();
      const stats = {
        total: db.prepare('SELECT COUNT(*) as count FROM auctions').get(),
        byBrand: db.prepare('SELECT search_term, COUNT(*) as count FROM auctions GROUP BY search_term').all(),
        avgPrice: db.prepare('SELECT AVG(current_price) as avg FROM auctions WHERE current_price > 0').get()
      };
      const duration = Date.now() - start;

      db.close();

      expect(stats.total.count).toBeGreaterThan(0);
      expect(stats.byBrand.length).toBeGreaterThan(0);
      expect(duration).toBeLessThan(150);
    });
  });

  describe('Memory Usage', () => {
    test('should not leak memory on repeated requests', async () => {
      const initialMemory = process.memoryUsage().heapUsed;

      // Make 100 requests
      for (let i = 0; i < 100; i++) {
        await request(app).get('/api/auctions');
      }

      // Force garbage collection if available
      if (global.gc) {
        global.gc();
      }

      const finalMemory = process.memoryUsage().heapUsed;
      const memoryIncrease = finalMemory - initialMemory;

      // Memory increase should be reasonable (< 50MB for 100 requests)
      expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024);
    }, 30000);

    test('should handle large response payloads', async () => {
      const response = await request(app).get('/api/auctions');

      expect(response.body.data.length).toBeGreaterThan(0);

      // Calculate payload size
      const payloadSize = JSON.stringify(response.body).length;

      // Payload should be reasonable (< 5MB)
      expect(payloadSize).toBeLessThan(5 * 1024 * 1024);
    });
  });

  describe('Throughput', () => {
    test('should maintain throughput under load', async () => {
      const requestCount = 100;
      const start = Date.now();

      const requests = Array.from({ length: requestCount }, () =>
        request(app).get('/api/auctions')
      );

      await Promise.all(requests);
      const duration = Date.now() - start;

      const throughput = (requestCount / duration) * 1000; // requests per second

      // Should handle at least 20 requests per second
      expect(throughput).toBeGreaterThan(20);
    }, 30000);
  });

  describe('Database Connection Management', () => {
    test('should open and close connections properly', async () => {
      // Make multiple requests
      for (let i = 0; i < 10; i++) {
        const response = await request(app).get('/api/auctions');
        expect(response.status).toBe(200);
      }

      // No connection leaks - all connections should be closed
      // This is implicit in the test passing without hanging
    });

    test('should handle rapid connection open/close', async () => {
      const start = Date.now();

      for (let i = 0; i < 50; i++) {
        const db = new Database(testDbPath, { readonly: true });
        db.prepare('SELECT COUNT(*) FROM auctions').get();
        db.close();
      }

      const duration = Date.now() - start;

      // Should handle 50 connection cycles quickly
      expect(duration).toBeLessThan(1000);
    });
  });
});