const request = require("supertest"); const app = require("../../server"); const redisClient = require("../../src/utils/redis-client"); const fallbackStore = require("../../src/utils/fallback-store"); describe("Performance and Load Testing", () => { let server; let testEventId = "777"; // Use a unique event ID for testing const CONCURRENT_REQUESTS = 100; // Test with 100 concurrent requests const TICKET_COUNT = 50; // Start with 50 tickets beforeAll(async () => { // Start the server server = app.listen(0); // Use random port // Wait for server to be ready await new Promise((resolve) => setTimeout(resolve, 1000)); // Ensure Redis is connected if (!redisClient.isConnected) { await redisClient.connect(); } }); afterAll(async () => { // Clean up test data try { if (redisClient.isConnected) { const testEventKey = `event:${testEventId}:meta`; const testTicketsKey = `event:${testEventId}:tickets`; await redisClient.client.del(testEventKey); await redisClient.client.del(testTicketsKey); } } catch (error) { console.warn("Failed to cleanup test data:", error.message); } // Close server if (server) { await new Promise((resolve) => server.close(resolve)); } // Disconnect Redis if (redisClient.isConnected) { await redisClient.disconnect(); } }); beforeEach(async () => { // Reset fallback store fallbackStore.deactivate(); fallbackStore.events.clear(); fallbackStore.globalStats = { totalEvents: 0, totalTickets: 0, totalSold: 0, lastSeeded: null, }; // Create test event with tickets in Redis if (redisClient.isConnected) { const testEventKey = `event:${testEventId}:meta`; const testTicketsKey = `event:${testEventId}:tickets`; // Create event metadata await redisClient.client.hSet(testEventKey, { eventId: testEventId, name: "Performance Test Event", description: "Test event for load testing", totalTickets: TICKET_COUNT.toString(), soldTickets: "0", createdAt: new Date().toISOString(), lastSoldAt: "never", }); // Create test tickets const testTickets = Array.from( { length: TICKET_COUNT }, (_, i) => `perf-ticket-${i + 1}` ); await redisClient.client.lPush(testTicketsKey, testTickets); } }); describe("High Concurrency Ticket Purchase", () => { test("should handle 100 concurrent requests without duplicate tickets", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } const soldTickets = new Set(); const purchasePromises = []; const startTime = Date.now(); // Create concurrent purchase requests for (let i = 0; i < CONCURRENT_REQUESTS; i++) { const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json") .then((response) => ({ success: true, response, index: i })) .catch((error) => ({ success: false, error, index: i })); purchasePromises.push(promise); } // Wait for all requests to complete const results = await Promise.all(purchasePromises); const endTime = Date.now(); const totalTime = endTime - startTime; let successCount = 0; let failureCount = 0; let duplicateCount = 0; results.forEach((result) => { if ( result.success && result.response.status === 200 && result.response.body.success ) { successCount++; const ticket = result.response.body.ticket; // Check for duplicates if (soldTickets.has(ticket)) { duplicateCount++; } else { soldTickets.add(ticket); } } else { failureCount++; } }); // Performance metrics const avgResponseTime = totalTime / CONCURRENT_REQUESTS; const requestsPerSecond = (CONCURRENT_REQUESTS / totalTime) * 1000; console.log(`\nšŸ“Š Performance Test Results:`); console.log(` Total Time: ${totalTime}ms`); console.log(` Average Response Time: ${avgResponseTime.toFixed(2)}ms`); console.log(` Requests per Second: ${requestsPerSecond.toFixed(2)}`); console.log(` Success Count: ${successCount}`); console.log(` Failure Count: ${failureCount}`); console.log(` Duplicate Tickets: ${duplicateCount}`); // Critical assertions expect(duplicateCount).toBe(0); // No duplicate tickets allowed expect(successCount).toBe(TICKET_COUNT); // Should sell exactly available tickets expect(failureCount).toBe(CONCURRENT_REQUESTS - TICKET_COUNT); // Remaining should fail expect(soldTickets.size).toBe(TICKET_COUNT); // All sold tickets should be unique // Performance requirements expect(avgResponseTime).toBeLessThan(1000); // Should respond within 1 second on average expect(requestsPerSecond).toBeGreaterThan(10); // Should handle at least 10 RPS }); test("should maintain data consistency under high load", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } // Purchase 20 tickets under load const purchasePromises = []; for (let i = 0; i < 20; i++) { const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); purchasePromises.push(promise); } await Promise.all(purchasePromises); // Verify Redis consistency const remainingTickets = await redisClient.client.lLen( `event:${testEventId}:tickets` ); const eventStats = await redisClient.getEventStats(testEventId); expect(remainingTickets).toBe(TICKET_COUNT - 20); expect(eventStats.soldTickets).toBe(20); expect(eventStats.remainingTickets).toBe(TICKET_COUNT - 20); // Verify no tickets remain after exhausting supply const finalPurchasePromises = []; for (let i = 0; i < TICKET_COUNT; i++) { const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); finalPurchasePromises.push(promise); } const finalResults = await Promise.all(finalPurchasePromises); const finalSuccessCount = finalResults.filter( (r) => r.response && r.response.status === 200 && r.response.body.success ).length; expect(finalSuccessCount).toBe(TICKET_COUNT - 20); // Should only sell remaining tickets }); }); describe("Fallback Store Performance", () => { test("should handle high concurrency in fallback mode", async () => { // Disconnect Redis to simulate failure if (redisClient.isConnected) { await redisClient.disconnect(); } // Seed fallback store with test event const metadata = { eventId: testEventId, name: "Fallback Performance Test Event", description: "Test event for fallback performance", totalTickets: 30, soldTickets: 0, createdAt: new Date().toISOString(), lastSoldAt: "never", }; const testTickets = Array.from( { length: 30 }, (_, i) => `fallback-perf-ticket-${i + 1}` ); fallbackStore.seedEvent(testEventId, testTickets, metadata); fallbackStore.activate("Performance test fallback mode"); const soldTickets = new Set(); const purchasePromises = []; const startTime = Date.now(); // Create 50 concurrent purchase requests (more than available) for (let i = 0; i < 50; i++) { const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json") .then((response) => ({ success: true, response, index: i })) .catch((error) => ({ success: false, error, index: i })); purchasePromises.push(promise); } const results = await Promise.all(purchasePromises); const endTime = Date.now(); const totalTime = endTime - startTime; let successCount = 0; let failureCount = 0; let duplicateCount = 0; results.forEach((result) => { if ( result.success && result.response.status === 200 && result.response.body.success ) { successCount++; const ticket = result.response.body.ticket; if (soldTickets.has(ticket)) { duplicateCount++; } else { soldTickets.add(ticket); } } else { failureCount++; } }); console.log(`\nšŸ“Š Fallback Performance Test Results:`); console.log(` Total Time: ${totalTime}ms`); console.log(` Success Count: ${successCount}`); console.log(` Failure Count: ${failureCount}`); console.log(` Duplicate Tickets: ${duplicateCount}`); // Critical assertions for fallback mode expect(duplicateCount).toBe(0); // No duplicate tickets expect(successCount).toBe(30); // Should sell exactly available tickets expect(failureCount).toBe(20); // Remaining should fail expect(soldTickets.size).toBe(30); // All sold tickets should be unique // Verify fallback store consistency const event = fallbackStore.events.get(testEventId); expect(event.tickets).toHaveLength(0); // No tickets should remain expect(event.soldTickets).toBe(30); // All tickets should be marked as sold }); }); describe("Memory and Resource Usage", () => { test("should maintain stable memory usage under load", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } const initialMemory = process.memoryUsage(); console.log(`\nšŸ’¾ Initial Memory Usage:`); console.log(` RSS: ${(initialMemory.rss / 1024 / 1024).toFixed(2)} MB`); console.log( ` Heap Used: ${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB` ); // Perform multiple rounds of purchases for (let round = 0; round < 3; round++) { // Reset test event const testTicketsKey = `event:${testEventId}:tickets`; await redisClient.client.del(testTicketsKey); const testTickets = Array.from( { length: 20 }, (_, i) => `round-${round}-ticket-${i + 1}` ); await redisClient.client.lPush(testTicketsKey, testTickets); // Purchase all tickets const purchasePromises = []; for (let i = 0; i < 20; i++) { const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); purchasePromises.push(promise); } await Promise.all(purchasePromises); } const finalMemory = process.memoryUsage(); console.log(`\nšŸ’¾ Final Memory Usage:`); console.log(` RSS: ${(finalMemory.rss / 1024 / 1024).toFixed(2)} MB`); console.log( ` Heap Used: ${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB` ); const memoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed; const memoryIncreaseMB = memoryIncrease / 1024 / 1024; console.log(`\nšŸ“ˆ Memory Change: ${memoryIncreaseMB.toFixed(2)} MB`); // Memory should not increase excessively (less than 50MB increase) expect(memoryIncreaseMB).toBeLessThan(50); }); }); describe("Response Time Consistency", () => { test("should maintain consistent response times under varying load", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } const responseTimes = []; const loadLevels = [10, 25, 50, 75, 100]; for (const loadLevel of loadLevels) { // Reset test event const testTicketsKey = `event:${testEventId}:tickets`; await redisClient.client.del(testTicketsKey); const testTickets = Array.from( { length: loadLevel }, (_, i) => `load-${loadLevel}-ticket-${i + 1}` ); await redisClient.client.lPush(testTicketsKey, testTickets); const startTime = Date.now(); const purchasePromises = []; for (let i = 0; i < loadLevel; i++) { const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); purchasePromises.push(promise); } await Promise.all(purchasePromises); const endTime = Date.now(); const responseTime = endTime - startTime; responseTimes.push({ loadLevel, responseTime, avgResponseTime: responseTime / loadLevel, }); console.log(`\n⚔ Load Level ${loadLevel}:`); console.log(` Total Time: ${responseTime}ms`); console.log( ` Average Response: ${(responseTime / loadLevel).toFixed(2)}ms` ); } // Calculate consistency metrics const avgResponseTimes = responseTimes.map((r) => r.avgResponseTime); const minResponseTime = Math.min(...avgResponseTimes); const maxResponseTime = Math.max(...avgResponseTimes); const responseTimeVariance = maxResponseTime - minResponseTime; console.log(`\nšŸ“Š Response Time Consistency:`); console.log(` Min Avg Response: ${minResponseTime.toFixed(2)}ms`); console.log(` Max Avg Response: ${maxResponseTime.toFixed(2)}ms`); console.log(` Variance: ${responseTimeVariance.toFixed(2)}ms`); // Response times should be reasonably consistent (variance < 200ms) expect(responseTimeVariance).toBeLessThan(200); // All response times should be reasonable (< 2 seconds average) avgResponseTimes.forEach((time) => { expect(time).toBeLessThan(2000); }); }); }); });