const request = require("supertest"); const app = require("../../server"); const redisClient = require("../../src/utils/redis-client"); const fallbackStore = require("../../src/utils/fallback-store"); describe("Duplicate Ticket Prevention - Integration Tests", () => { let server; let testEventId = "999"; // Use a unique event ID for testing 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 10 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: "Test Event for Duplicate Prevention", description: "Test event to verify no duplicate tickets", totalTickets: "10", soldTickets: "0", createdAt: new Date().toISOString(), lastSoldAt: "never", }); // Create 10 test tickets const testTickets = Array.from( { length: 10 }, (_, i) => `test-ticket-${i + 1}` ); await redisClient.client.lPush(testTicketsKey, testTickets); // Update global stats await redisClient.client.hSet("global:stats", { totalEvents: "1", totalTickets: "10", totalSold: "0", lastSeeded: new Date().toISOString(), }); } }); describe("Redis Ticket Purchase - Duplicate Prevention", () => { test("should prevent duplicate ticket sales under normal conditions", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } const purchaseIds = []; const soldTickets = new Set(); let successCount = 0; let failureCount = 0; // Attempt to purchase 15 tickets (more than available) for (let i = 0; i < 15; i++) { const purchaseId = `test-purchase-${Date.now()}-${i}`; purchaseIds.push(purchaseId); try { const response = await request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); if (response.status === 200 && response.body.success) { successCount++; const ticket = response.body.ticket; // Verify ticket is unique expect(soldTickets.has(ticket)).toBe(false); soldTickets.add(ticket); expect(response.body.usingFallback).toBe(false); } else { failureCount++; expect(response.status).toBe(409); // No tickets available expect(response.body.message).toContain("No tickets available"); } } catch (error) { failureCount++; } } // Should have sold exactly 10 tickets (no duplicates) expect(successCount).toBe(10); expect(failureCount).toBe(5); expect(soldTickets.size).toBe(10); // Verify no tickets remain const remainingTickets = await redisClient.client.lLen( `event:${testEventId}:tickets` ); expect(remainingTickets).toBe(0); }); test("should prevent duplicate tickets under concurrent load", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } // Reset test event with 5 tickets const testTicketsKey = `event:${testEventId}:tickets`; await redisClient.client.del(testTicketsKey); const testTickets = Array.from( { length: 5 }, (_, i) => `concurrent-ticket-${i + 1}` ); await redisClient.client.lPush(testTicketsKey, testTickets); const soldTickets = new Set(); const purchasePromises = []; // Create 10 concurrent purchase requests for (let i = 0; i < 10; i++) { const purchaseId = `concurrent-purchase-${Date.now()}-${i}`; const promise = request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json") .then((response) => ({ success: true, response, purchaseId })) .catch((error) => ({ success: false, error, purchaseId })); purchasePromises.push(promise); } // Wait for all requests to complete const results = await Promise.all(purchasePromises); let successCount = 0; let failureCount = 0; results.forEach((result) => { if ( result.success && result.response.status === 200 && result.response.body.success ) { successCount++; const ticket = result.response.body.ticket; // Verify ticket is unique expect(soldTickets.has(ticket)).toBe(false); soldTickets.add(ticket); } else { failureCount++; } }); // Should have sold exactly 5 tickets (no duplicates) expect(successCount).toBe(5); expect(failureCount).toBe(5); expect(soldTickets.size).toBe(5); // Verify no tickets remain const remainingTickets = await redisClient.client.lLen(testTicketsKey); expect(remainingTickets).toBe(0); }); }); describe("Fallback Store - Duplicate Prevention", () => { test("should prevent duplicate tickets in fallback mode", async () => { // Seed fallback store with test event const metadata = { eventId: testEventId, name: "Test Event for Duplicate Prevention", description: "Test event to verify no duplicate tickets", totalTickets: 5, soldTickets: 0, createdAt: new Date().toISOString(), lastSoldAt: "never", }; const testTickets = Array.from( { length: 5 }, (_, i) => `fallback-ticket-${i + 1}` ); fallbackStore.seedEvent(testEventId, testTickets, metadata); fallbackStore.activate("Test activation"); const soldTickets = new Set(); let successCount = 0; let failureCount = 0; // Attempt to purchase 8 tickets (more than available) for (let i = 0; i < 8; i++) { const purchaseId = `fallback-purchase-${Date.now()}-${i}`; try { const response = await request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); if (response.status === 200 && response.body.success) { successCount++; const ticket = response.body.ticket; // Verify ticket is unique expect(soldTickets.has(ticket)).toBe(false); soldTickets.add(ticket); expect(response.body.usingFallback).toBe(true); } else { failureCount++; expect(response.status).toBe(409); // No tickets available expect(response.body.message).toContain("No tickets available"); } } catch (error) { failureCount++; } } // Should have sold exactly 5 tickets (no duplicates) expect(successCount).toBe(5); expect(failureCount).toBe(3); expect(soldTickets.size).toBe(5); // Verify no tickets remain in fallback store const event = fallbackStore.events.get(testEventId); expect(event.tickets).toHaveLength(0); }); }); describe("Mixed Mode - Redis + Fallback", () => { test("should prevent duplicates when switching between Redis and fallback", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } // Start with Redis const soldTickets = new Set(); // Purchase 3 tickets from Redis for (let i = 0; i < 3; i++) { const purchaseId = `mixed-purchase-redis-${Date.now()}-${i}`; const response = await request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.usingFallback).toBe(false); const ticket = response.body.ticket; expect(soldTickets.has(ticket)).toBe(false); soldTickets.add(ticket); } // Simulate Redis failure by disconnecting await redisClient.disconnect(); // Purchase remaining tickets from fallback for (let i = 0; i < 7; i++) { const purchaseId = `mixed-purchase-fallback-${Date.now()}-${i}`; const response = await request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.usingFallback).toBe(true); const ticket = response.body.ticket; expect(soldTickets.has(ticket)).toBe(false); soldTickets.add(ticket); } // Verify total unique tickets sold expect(soldTickets.size).toBe(10); // Try to purchase one more (should fail) const response = await request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); expect(response.status).toBe(409); expect(response.body.success).toBe(false); expect(response.body.message).toContain("No tickets available"); }); }); describe("Data Integrity Verification", () => { test("should maintain consistent ticket counts across Redis and fallback", async () => { if (!redisClient.isConnected) { console.warn("Skipping Redis test - Redis not connected"); return; } // Purchase 3 tickets for (let i = 0; i < 3; i++) { const purchaseId = `integrity-purchase-${Date.now()}-${i}`; const response = await request(server) .post(`/buy/${testEventId}`) .set("Content-Type", "application/json"); expect(response.status).toBe(200); expect(response.body.success).toBe(true); } // Check Redis stats const redisStats = await redisClient.getEventStats(testEventId); expect(redisStats.remainingTickets).toBe(7); expect(redisStats.soldTickets).toBe(3); // Check fallback store stats (should be synced) fallbackStore.activate("Test activation"); const fallbackStats = fallbackStore.getEventStats(testEventId); expect(fallbackStats.remainingTickets).toBe(7); expect(fallbackStats.soldTickets).toBe(3); // Verify global stats const globalStats = await redisClient.getGlobalStats(); expect(globalStats.totalSold).toBe(3); }); }); });