feat: add integration and setup tests and complete code review fixes

This commit is contained in:
Ayobami
2025-08-14 22:41:48 +01:00
parent da78487047
commit 06f0cc3638
15 changed files with 2766 additions and 263 deletions
+285 -253
View File
@@ -8,13 +8,18 @@ const fallbackStore = require("./src/utils/fallback-store");
const logger = require("./src/utils/logger");
const pdfGenerator = require("./src/utils/pdf-generator");
const metrics = require("./src/utils/metrics");
const security = require("./src/utils/security");
const app = express();
const port = process.env.PORT || 3049;
// Middleware
app.use(express.json());
app.use(express.static("public"));
// Security middleware
app.use(security.securityHeaders);
app.use(security.requestSizeLimit);
app.use(security.securityLogging);
// Request logging middleware
app.use((req, res, next) => {
@@ -29,6 +34,9 @@ app.use((req, res, next) => {
// Prometheus metrics middleware
app.use(metrics.metricsMiddleware);
// Apply general rate limiting to all routes
app.use(security.generalLimiter);
// Health check endpoint
app.get("/health", async (req, res) => {
const redisHealthy = redisClient.isHealthy();
@@ -75,7 +83,7 @@ app.get("/events", async (req, res) => {
});
// Get specific event stats
app.get("/events/:eventId", async (req, res) => {
app.get("/events/:eventId", security.validateEventId, async (req, res) => {
try {
const eventId = req.params.eventId;
let eventStats;
@@ -108,109 +116,256 @@ app.get("/events/:eventId", async (req, res) => {
});
// Purchase ticket endpoint (multi-event)
app.post("/buy/:eventId", async (req, res) => {
const startTime = Date.now();
const eventId = req.params.eventId;
const purchaseId = uuidv4();
const timestamp = new Date().toISOString();
app.post(
"/buy/:eventId",
security.purchaseLimiter,
security.validateEventId,
async (req, res) => {
const startTime = Date.now();
const eventId = req.params.eventId;
const purchaseId = uuidv4();
const timestamp = new Date().toISOString();
try {
let result;
try {
let result;
// Try Redis first
if (redisClient.isHealthy()) {
// Try Redis first
if (redisClient.isHealthy()) {
try {
const luaResult = await redisClient.purchaseTicket(
eventId,
purchaseId,
timestamp
);
if (luaResult[0]) {
// Success - generate PDF ticket
try {
// Get event details for PDF
const eventStats = await redisClient.getEventStats(eventId);
const pdfStartTime = Date.now();
const pdfResult = await pdfGenerator.generateTicketPDF({
ticketId: luaResult[0],
eventId,
purchaseId,
eventName: eventStats?.name || `Event ${eventId}`,
eventDescription:
eventStats?.description || "Event description not available",
timestamp,
soldCount: luaResult[2],
});
// Record PDF generation metrics
const pdfDuration = (Date.now() - pdfStartTime) / 1000;
metrics.recordPDFGeneration(
pdfResult.success ? "success" : "failed",
pdfDuration
);
result = {
success: true,
ticket: luaResult[0],
purchaseId,
eventId,
soldCount: luaResult[2],
message: "Ticket purchased successfully!",
usingFallback: false,
pdf: {
generated: pdfResult.success,
filename: pdfResult.filename,
downloadUrl: `/tickets/${purchaseId}`,
},
};
// Record metrics for successful purchase
metrics.recordTicketSale(eventId, "success");
metrics.updateTicketMetrics(
eventId,
luaResult[2],
luaResult[3] || 0
);
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
} catch (pdfError) {
logger.error("PDF generation failed:", pdfError);
// Still return success for ticket purchase, but note PDF failure
result = {
success: true,
ticket: luaResult[0],
purchaseId,
eventId,
soldCount: luaResult[2],
message:
"Ticket purchased successfully! (PDF generation failed)",
usingFallback: false,
pdf: {
generated: false,
error: "PDF generation failed",
},
};
// Record metrics for successful purchase (even with PDF failure)
metrics.recordTicketSale(eventId, "success");
metrics.updateTicketMetrics(
eventId,
luaResult[2],
luaResult[3] || 0
);
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
}
} else {
// Failed - handle specific error
const errorCode = luaResult[1];
let statusCode = 400;
let message = "Purchase failed";
switch (errorCode) {
case "EVENT_NOT_FOUND":
statusCode = 404;
message = "Event not found";
break;
case "NO_TICKETS_AVAILABLE":
statusCode = 409;
message = "No tickets available for this event";
break;
}
// Record metrics for failed purchase
metrics.recordTicketSale(eventId, "failed");
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
return res.status(statusCode).json({
success: false,
message,
errorCode,
eventId,
purchaseId,
});
}
} catch (redisError) {
logger.error(
"Redis purchase failed, attempting fallback:",
redisError
);
// Activate fallback if not already active
if (!fallbackStore.isActive) {
fallbackStore.activate("Redis purchase operation failed");
// Try to sync with Redis data if possible
setTimeout(() => {
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
fallbackStore.attemptReseed();
}
}, 100);
}
throw redisError; // Will be caught by outer try-catch for fallback
}
} else {
throw new Error("Redis not available");
}
const responseTime = Date.now() - startTime;
result.responseTime = `${responseTime}ms`;
res.json(result);
} catch (error) {
// Fallback to in-memory store
try {
const luaResult = await redisClient.purchaseTicket(
if (!fallbackStore.isActive) {
fallbackStore.activate("Redis connection failed during purchase");
// Try to sync with Redis data if possible
setTimeout(() => {
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
fallbackStore.attemptReseed();
}
}, 100);
}
const fallbackResult = fallbackStore.purchaseTicket(
eventId,
purchaseId,
timestamp
purchaseId
);
if (luaResult[0]) {
// Success - generate PDF ticket
if (fallbackResult.success) {
// Generate PDF for fallback purchase
try {
// Get event details for PDF
const eventStats = await redisClient.getEventStats(eventId);
const eventStats = fallbackStore.getEventStats(eventId);
const pdfStartTime = Date.now();
const pdfResult = await pdfGenerator.generateTicketPDF({
ticketId: luaResult[0],
ticketId: fallbackResult.ticket,
eventId,
purchaseId,
eventName: eventStats?.name || `Event ${eventId}`,
eventDescription:
eventStats?.description || "Event description not available",
timestamp,
soldCount: luaResult[2],
soldCount: fallbackResult.soldCount,
});
// Record PDF generation metrics
const pdfDuration = (Date.now() - pdfStartTime) / 1000;
metrics.recordPDFGeneration(
pdfResult.success ? "success" : "failed",
pdfDuration
);
result = {
const responseTime = Date.now() - startTime;
res.json({
success: true,
ticket: luaResult[0],
ticket: fallbackResult.ticket,
purchaseId,
eventId,
soldCount: luaResult[2],
message: "Ticket purchased successfully!",
usingFallback: false,
soldCount: fallbackResult.soldCount,
message: "Ticket purchased successfully (fallback mode)!",
usingFallback: true,
responseTime: `${responseTime}ms`,
pdf: {
generated: pdfResult.success,
filename: pdfResult.filename,
downloadUrl: `/tickets/${purchaseId}`,
},
};
});
// Record metrics for successful purchase
metrics.recordTicketSale(eventId, "success");
// Record metrics for successful fallback purchase
metrics.recordTicketSale(eventId, "success_fallback");
metrics.updateTicketMetrics(
eventId,
luaResult[2],
luaResult[3] || 0
fallbackResult.soldCount,
fallbackResult.remainingTickets || 0
);
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
logger.info(
`PDF ticket generated for fallback purchase ${purchaseId}`
);
} catch (pdfError) {
logger.error("PDF generation failed:", pdfError);
logger.error("PDF generation failed in fallback mode:", pdfError);
// Still return success for ticket purchase, but note PDF failure
result = {
const responseTime = Date.now() - startTime;
res.json({
success: true,
ticket: luaResult[0],
ticket: fallbackResult.ticket,
purchaseId,
eventId,
soldCount: luaResult[2],
message: "Ticket purchased successfully! (PDF generation failed)",
usingFallback: false,
soldCount: fallbackResult.soldCount,
message:
"Ticket purchased successfully (fallback mode, PDF generation failed)!",
usingFallback: true,
responseTime: `${responseTime}ms`,
pdf: {
generated: false,
error: "PDF generation failed",
},
};
});
// Record metrics for successful purchase (even with PDF failure)
metrics.recordTicketSale(eventId, "success");
// Record metrics for successful fallback purchase (even with PDF failure)
metrics.recordTicketSale(eventId, "success_fallback");
metrics.updateTicketMetrics(
eventId,
luaResult[2],
luaResult[3] || 0
fallbackResult.soldCount,
fallbackResult.remainingTickets || 0
);
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
}
} else {
// Failed - handle specific error
const errorCode = luaResult[1];
let statusCode = 400;
let message = "Purchase failed";
switch (errorCode) {
switch (fallbackResult.error) {
case "EVENT_NOT_FOUND":
statusCode = 404;
message = "Event not found";
@@ -221,212 +376,84 @@ app.post("/buy/:eventId", async (req, res) => {
break;
}
// Record metrics for failed purchase
metrics.recordTicketSale(eventId, "failed");
// Record metrics for failed fallback purchase
metrics.recordTicketSale(eventId, "failed_fallback");
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
return res.status(statusCode).json({
logger.logPurchase(
eventId,
null,
purchaseId,
false,
fallbackResult.error
);
res.status(statusCode).json({
success: false,
message,
errorCode,
errorCode: fallbackResult.error,
eventId,
purchaseId,
});
}
} catch (redisError) {
logger.error("Redis purchase failed, attempting fallback:", redisError);
// Activate fallback if not already active
if (!fallbackStore.isActive) {
fallbackStore.activate("Redis purchase operation failed");
// Try to sync with Redis data if possible
setTimeout(() => {
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
fallbackStore.attemptReseed();
}
}, 100);
}
throw redisError; // Will be caught by outer try-catch for fallback
}
} else {
throw new Error("Redis not available");
}
const responseTime = Date.now() - startTime;
result.responseTime = `${responseTime}ms`;
res.json(result);
} catch (error) {
// Fallback to in-memory store
try {
if (!fallbackStore.isActive) {
fallbackStore.activate("Redis connection failed during purchase");
// Try to sync with Redis data if possible
setTimeout(() => {
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
fallbackStore.attemptReseed();
}
}, 100);
}
const fallbackResult = fallbackStore.purchaseTicket(eventId, purchaseId);
if (fallbackResult.success) {
// Generate PDF for fallback purchase
try {
const eventStats = fallbackStore.getEventStats(eventId);
const pdfResult = await pdfGenerator.generateTicketPDF({
ticketId: fallbackResult.ticket,
eventId,
purchaseId,
eventName: eventStats?.name || `Event ${eventId}`,
eventDescription:
eventStats?.description || "Event description not available",
timestamp,
soldCount: fallbackResult.soldCount,
});
const responseTime = Date.now() - startTime;
res.json({
success: true,
ticket: fallbackResult.ticket,
purchaseId,
eventId,
soldCount: fallbackResult.soldCount,
message: "Ticket purchased successfully (fallback mode)!",
usingFallback: true,
responseTime: `${responseTime}ms`,
pdf: {
generated: pdfResult.success,
filename: pdfResult.filename,
downloadUrl: `/tickets/${purchaseId}`,
},
});
// Record metrics for successful fallback purchase
metrics.recordTicketSale(eventId, "success_fallback");
metrics.updateTicketMetrics(
eventId,
fallbackResult.soldCount,
fallbackResult.remainingTickets || 0
);
logger.info(
`PDF ticket generated for fallback purchase ${purchaseId}`
);
} catch (pdfError) {
logger.error("PDF generation failed in fallback mode:", pdfError);
const responseTime = Date.now() - startTime;
res.json({
success: true,
ticket: fallbackResult.ticket,
purchaseId,
eventId,
soldCount: fallbackResult.soldCount,
message:
"Ticket purchased successfully (fallback mode, PDF generation failed)!",
usingFallback: true,
responseTime: `${responseTime}ms`,
pdf: {
generated: false,
error: "PDF generation failed",
},
});
// Record metrics for successful fallback purchase (even with PDF failure)
metrics.recordTicketSale(eventId, "success_fallback");
metrics.updateTicketMetrics(
eventId,
fallbackResult.soldCount,
fallbackResult.remainingTickets || 0
);
}
} else {
let statusCode = 400;
let message = "Purchase failed";
} catch (fallbackError) {
logger.error("Both Redis and fallback failed:", fallbackError);
switch (fallbackResult.error) {
case "EVENT_NOT_FOUND":
statusCode = 404;
message = "Event not found";
break;
case "NO_TICKETS_AVAILABLE":
statusCode = 409;
message = "No tickets available for this event";
break;
}
// Record metrics for system failure
metrics.recordTicketSale(eventId, "system_error");
// Record metrics for failed fallback purchase
metrics.recordTicketSale(eventId, "failed_fallback");
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
logger.logPurchase(
eventId,
null,
purchaseId,
false,
fallbackResult.error
);
res.status(statusCode).json({
res.status(500).json({
success: false,
message,
errorCode: fallbackResult.error,
message: "System temporarily unavailable",
eventId,
purchaseId,
usingFallback: true,
});
}
} catch (fallbackError) {
logger.error("Both Redis and fallback failed:", fallbackError);
// Record metrics for system failure
metrics.recordTicketSale(eventId, "system_error");
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
res.status(500).json({
success: false,
message: "System temporarily unavailable",
eventId,
purchaseId,
});
}
}
});
);
// Download ticket PDF endpoint
app.get("/tickets/:purchaseId", async (req, res) => {
try {
const purchaseId = req.params.purchaseId;
app.get(
"/tickets/:purchaseId",
security.validatePurchaseId,
async (req, res) => {
try {
const purchaseId = req.params.purchaseId;
if (!pdfGenerator.ticketExists(purchaseId)) {
return res.status(404).json({
if (!pdfGenerator.ticketExists(purchaseId)) {
return res.status(404).json({
success: false,
message: "Ticket not found",
});
}
const filepath = pdfGenerator.getTicketPath(purchaseId);
const filename = `ticket-${purchaseId}.pdf`;
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename="${filename}"`
);
const fileStream = require("fs").createReadStream(filepath);
fileStream.pipe(res);
logger.info(`PDF ticket downloaded: ${purchaseId}`);
} catch (error) {
logger.error("Error downloading ticket:", error);
res.status(500).json({
success: false,
message: "Ticket not found",
message: "Failed to download ticket",
});
}
const filepath = pdfGenerator.getTicketPath(purchaseId);
const filename = `ticket-${purchaseId}.pdf`;
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
const fileStream = require("fs").createReadStream(filepath);
fileStream.pipe(res);
logger.info(`PDF ticket downloaded: ${purchaseId}`);
} catch (error) {
logger.error("Error downloading ticket:", error);
res.status(500).json({
success: false,
message: "Failed to download ticket",
});
}
});
);
// PDF management endpoint
app.get("/admin/pdf-stats", async (req, res) => {
app.get("/admin/pdf-stats", security.adminLimiter, async (req, res) => {
try {
const stats = pdfGenerator.getStats();
res.json({
@@ -443,27 +470,32 @@ app.get("/admin/pdf-stats", async (req, res) => {
});
// Cleanup old tickets endpoint
app.post("/admin/cleanup-tickets", async (req, res) => {
try {
const maxAgeHours = req.body.maxAgeHours || 24;
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
app.post(
"/admin/cleanup-tickets",
security.adminLimiter,
security.validateCleanupRequest,
async (req, res) => {
try {
const maxAgeHours = req.body.maxAgeHours || 24;
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
res.json({
success: true,
message: `Cleaned up ${deletedCount} old tickets`,
deletedCount,
});
} catch (error) {
logger.error("Error cleaning up tickets:", error);
res.status(500).json({
success: false,
message: "Failed to cleanup tickets",
});
res.json({
success: true,
message: `Cleaned up ${deletedCount} old tickets`,
deletedCount,
});
} catch (error) {
logger.error("Error cleaning up tickets:", error);
res.status(500).json({
success: false,
message: "Failed to cleanup tickets",
});
}
}
});
);
// Seed fallback store endpoint
app.post("/admin/seed-fallback", async (req, res) => {
app.post("/admin/seed-fallback", security.adminLimiter, async (req, res) => {
try {
if (redisClient.isHealthy()) {
// Activate fallback store temporarily for seeding