feat: add integration and setup tests and complete code review fixes
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user