← 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();
});
});
});