← back to Handbag Auth Nextjs

auction-viewer/tests/integration/api.test.js

417 lines

/**
 * Integration Tests - API Endpoints
 */

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

describe('API Integration Tests', () => {
  let app;
  let testDb;
  let testDbPath;
  const testCsvPath = path.join(__dirname, '../fixtures/test-auction-history.csv');
  const testLogPath = path.join(__dirname, '../fixtures/test-scraper.log');

  beforeAll(() => {
    // Initialize test database
    testDb = new TestDatabase();
    testDb.initialize();
    testDb.seed(mockAuctions);
    testDbPath = testDb.getPath();

    // Create test CSV file
    const csvContent = `scrape_date,auction_id,title,auction_house,current_price,estimate_low,estimate_high,end_date,lot_number,url,search_term
2024-01-01 12:00:00,test-123,Test Bag,Christie's,5000,6000,8000,2024-01-15,123,https://test.com,Test Brand`;
    fs.writeFileSync(testCsvPath, csvContent);

    // Create test log file
    const logContent = `[2024-01-01 12:00:00] Test log entry 1
[2024-01-01 12:01:00] Test log entry 2
[2024-01-01 12:02:00] Test log entry 3`;
    fs.writeFileSync(testLogPath, logContent);

    // Create Express app with test endpoints
    app = express();
    app.use(cors());
    app.use(express.json());

    // API: Get all auctions
    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: [] });
      }
    });

    // API: Get best value auctions
    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: [] });
      }
    });

    // API: Get statistics
    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();

        const priceRange = db.prepare(`
          SELECT
            MIN(current_price) as min_price,
            MAX(current_price) as max_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,
            priceRange: priceRange || { min_price: 0, max_price: 0 }
          }
        });
      } catch (error) {
        res.json({ success: false, error: error.message });
      }
    });

    // API: Get logs
    app.get('/api/logs', (req, res) => {
      try {
        const logs = fs.readFileSync(testLogPath, 'utf8').split('\n').slice(-100).reverse();
        res.json({ success: true, logs });
      } catch (error) {
        res.json({ success: false, error: error.message, logs: [] });
      }
    });

    // API: Get CSV
    app.get('/api/csv', (req, res) => {
      try {
        const csvData = fs.readFileSync(testCsvPath, 'utf8');
        res.json({ success: true, data: csvData });
      } catch (error) {
        res.json({ success: false, error: error.message, data: '' });
      }
    });

    // API: System status
    app.get('/api/status', (req, res) => {
      const status = {
        database: fs.existsSync(testDbPath),
        csv: fs.existsSync(testCsvPath),
        logs: fs.existsSync(testLogPath),
        dbSize: fs.existsSync(testDbPath) ? fs.statSync(testDbPath).size : 0,
        csvSize: fs.existsSync(testCsvPath) ? fs.statSync(testCsvPath).size : 0,
        logSize: fs.existsSync(testLogPath) ? fs.statSync(testLogPath).size : 0,
      };
      res.json({ success: true, status });
    });
  });

  afterAll(() => {
    testDb.cleanup();
    if (fs.existsSync(testCsvPath)) fs.unlinkSync(testCsvPath);
    if (fs.existsSync(testLogPath)) fs.unlinkSync(testLogPath);
  });

  describe('GET /api/auctions', () => {
    test('should return 200 status', async () => {
      const response = await request(app).get('/api/auctions');
      expect(response.status).toBe(200);
    });

    test('should return success response', async () => {
      const response = await request(app).get('/api/auctions');
      expect(response.body.success).toBe(true);
    });

    test('should return array of auctions', async () => {
      const response = await request(app).get('/api/auctions');
      expect(Array.isArray(response.body.data)).toBe(true);
      expect(response.body.data.length).toBeGreaterThan(0);
    });

    test('should return correct count', async () => {
      const response = await request(app).get('/api/auctions');
      expect(response.body.count).toBe(response.body.data.length);
    });

    test('should include all required fields', async () => {
      const response = await request(app).get('/api/auctions');
      const auction = response.body.data[0];

      expect(auction).toHaveProperty('id');
      expect(auction).toHaveProperty('auction_id');
      expect(auction).toHaveProperty('title');
      expect(auction).toHaveProperty('auction_house');
      expect(auction).toHaveProperty('current_price');
      expect(auction).toHaveProperty('estimate_low');
      expect(auction).toHaveProperty('estimate_high');
    });

    test('should limit to 1000 results', async () => {
      const response = await request(app).get('/api/auctions');
      expect(response.body.data.length).toBeLessThanOrEqual(1000);
    });

    test('should have correct content type', async () => {
      const response = await request(app).get('/api/auctions');
      expect(response.headers['content-type']).toMatch(/json/);
    });
  });

  describe('GET /api/best-values', () => {
    test('should return 200 status', async () => {
      const response = await request(app).get('/api/best-values');
      expect(response.status).toBe(200);
    });

    test('should sort by percentage by default', async () => {
      const response = await request(app).get('/api/best-values');
      expect(response.body.success).toBe(true);
      expect(response.body.sortedBy).toBe('percent');

      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 sort by dollar amount when specified', async () => {
      const response = await request(app).get('/api/best-values?sort=dollar');
      expect(response.body.sortedBy).toBe('dollar');

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

    test('should include savings calculations', async () => {
      const response = await request(app).get('/api/best-values');
      const auction = response.body.data[0];

      expect(auction).toHaveProperty('savings_percent');
      expect(auction).toHaveProperty('savings_amount');
      expect(typeof auction.savings_percent).toBe('number');
      expect(typeof auction.savings_amount).toBe('number');
    });

    test('should only include valid prices', async () => {
      const response = await request(app).get('/api/best-values');

      response.body.data.forEach(auction => {
        expect(auction.current_price).toBeGreaterThan(0);
        expect(auction.estimate_low).toBeGreaterThan(0);
      });
    });

    test('should limit to 100 results', async () => {
      const response = await request(app).get('/api/best-values');
      expect(response.body.data.length).toBeLessThanOrEqual(100);
    });
  });

  describe('GET /api/stats', () => {
    test('should return 200 status', async () => {
      const response = await request(app).get('/api/stats');
      expect(response.status).toBe(200);
    });

    test('should return statistics object', async () => {
      const response = await request(app).get('/api/stats');
      expect(response.body.success).toBe(true);
      expect(response.body).toHaveProperty('stats');
    });

    test('should include total count', async () => {
      const response = await request(app).get('/api/stats');
      expect(response.body.stats.total).toBeGreaterThan(0);
    });

    test('should include brand breakdown', async () => {
      const response = await request(app).get('/api/stats');
      expect(Array.isArray(response.body.stats.byBrand)).toBe(true);
      expect(response.body.stats.byBrand.length).toBeGreaterThan(0);
    });

    test('should include auction house breakdown', async () => {
      const response = await request(app).get('/api/stats');
      expect(Array.isArray(response.body.stats.byAuctionHouse)).toBe(true);
      expect(response.body.stats.byAuctionHouse.length).toBeGreaterThan(0);
    });

    test('should include average price', async () => {
      const response = await request(app).get('/api/stats');
      expect(response.body.stats.avgPrice).toBeGreaterThan(0);
    });

    test('should include price range', async () => {
      const response = await request(app).get('/api/stats');
      expect(response.body.stats.priceRange).toHaveProperty('min_price');
      expect(response.body.stats.priceRange).toHaveProperty('max_price');
      expect(response.body.stats.priceRange.max_price).toBeGreaterThan(response.body.stats.priceRange.min_price);
    });
  });

  describe('GET /api/logs', () => {
    test('should return 200 status', async () => {
      const response = await request(app).get('/api/logs');
      expect(response.status).toBe(200);
    });

    test('should return array of logs', async () => {
      const response = await request(app).get('/api/logs');
      expect(response.body.success).toBe(true);
      expect(Array.isArray(response.body.logs)).toBe(true);
    });

    test('should limit to 100 log entries', async () => {
      const response = await request(app).get('/api/logs');
      expect(response.body.logs.length).toBeLessThanOrEqual(100);
    });

    test('should return logs in reverse order', async () => {
      const response = await request(app).get('/api/logs');
      expect(response.body.logs.length).toBeGreaterThan(0);
    });
  });

  describe('GET /api/csv', () => {
    test('should return 200 status', async () => {
      const response = await request(app).get('/api/csv');
      expect(response.status).toBe(200);
    });

    test('should return CSV data', async () => {
      const response = await request(app).get('/api/csv');
      expect(response.body.success).toBe(true);
      expect(typeof response.body.data).toBe('string');
      expect(response.body.data.length).toBeGreaterThan(0);
    });

    test('should include CSV headers', async () => {
      const response = await request(app).get('/api/csv');
      expect(response.body.data).toContain('scrape_date');
      expect(response.body.data).toContain('auction_id');
      expect(response.body.data).toContain('title');
    });
  });

  describe('GET /api/status', () => {
    test('should return 200 status', async () => {
      const response = await request(app).get('/api/status');
      expect(response.status).toBe(200);
    });

    test('should return status object', async () => {
      const response = await request(app).get('/api/status');
      expect(response.body.success).toBe(true);
      expect(response.body).toHaveProperty('status');
    });

    test('should check database existence', async () => {
      const response = await request(app).get('/api/status');
      expect(response.body.status.database).toBe(true);
    });

    test('should check CSV existence', async () => {
      const response = await request(app).get('/api/status');
      expect(response.body.status.csv).toBe(true);
    });

    test('should check logs existence', async () => {
      const response = await request(app).get('/api/status');
      expect(response.body.status.logs).toBe(true);
    });

    test('should include file sizes', async () => {
      const response = await request(app).get('/api/status');
      expect(response.body.status.dbSize).toBeGreaterThan(0);
      expect(response.body.status.csvSize).toBeGreaterThan(0);
      expect(response.body.status.logSize).toBeGreaterThan(0);
    });
  });

  describe('Error Handling', () => {
    test('should handle invalid endpoints gracefully', async () => {
      const response = await request(app).get('/api/nonexistent');
      expect(response.status).toBe(404);
    });

    test('should handle malformed query parameters', async () => {
      const response = await request(app).get('/api/best-values?sort=invalid');
      expect(response.status).toBe(200); // Should still work, default to percent
    });
  });

  describe('CORS', () => {
    test('should have CORS enabled', async () => {
      const response = await request(app).get('/api/auctions');
      expect(response.headers['access-control-allow-origin']).toBeDefined();
    });
  });
});