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
+374
View File
@@ -0,0 +1,374 @@
const request = require("supertest");
const app = require("../../server");
const redisClient = require("../../src/utils/redis-client");
const fallbackStore = require("../../src/utils/fallback-store");
describe("API Endpoints - Integration Tests", () => {
let server;
let testEventId = "888"; // 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 5 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 API Testing",
description: "Test event to verify API endpoints",
totalTickets: "5",
soldTickets: "0",
createdAt: new Date().toISOString(),
lastSoldAt: "never",
});
// Create 5 test tickets
const testTickets = Array.from(
{ length: 5 },
(_, i) => `api-test-ticket-${i + 1}`
);
await redisClient.client.lPush(testTicketsKey, testTickets);
}
});
describe("Health Check Endpoint", () => {
test("GET /health should return system status", async () => {
const response = await request(server).get("/health").expect(200);
expect(response.body).toHaveProperty("status", "ok");
expect(response.body).toHaveProperty("timestamp");
expect(response.body).toHaveProperty("redis");
expect(response.body).toHaveProperty("uptime");
expect(response.body.redis).toHaveProperty("connected");
expect(response.body.redis).toHaveProperty("fallbackActive");
});
});
describe("Events Endpoints", () => {
test("GET /events should return all events", async () => {
const response = await request(server).get("/events").expect(200);
expect(response.body).toHaveProperty("success", true);
expect(response.body).toHaveProperty("events");
expect(response.body).toHaveProperty("usingFallback");
expect(Array.isArray(response.body.events)).toBe(true);
});
test("GET /events/:eventId should return specific event", async () => {
const response = await request(server)
.get(`/events/${testEventId}`)
.expect(200);
expect(response.body).toHaveProperty("success", true);
expect(response.body).toHaveProperty("event");
expect(response.body.event).toHaveProperty("eventId", testEventId);
expect(response.body.event).toHaveProperty("name");
expect(response.body.event).toHaveProperty("totalTickets", 5);
expect(response.body.event).toHaveProperty("remainingTickets", 5);
});
test("GET /events/:eventId should return 404 for non-existent event", async () => {
const response = await request(server).get("/events/99999").expect(404);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty("message", "Event not found");
});
test("GET /events/:eventId should validate event ID format", async () => {
const response = await request(server).get("/events/invalid").expect(400);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty("message", "Invalid event ID");
});
});
describe("Ticket Purchase Endpoint", () => {
test("POST /buy/:eventId should purchase ticket successfully", async () => {
const response = await request(server)
.post(`/buy/${testEventId}`)
.set("Content-Type", "application/json")
.expect(200);
expect(response.body).toHaveProperty("success", true);
expect(response.body).toHaveProperty("ticket");
expect(response.body).toHaveProperty("purchaseId");
expect(response.body).toHaveProperty("eventId", testEventId);
expect(response.body).toHaveProperty("soldCount", 1);
expect(response.body).toHaveProperty("usingFallback", false);
expect(response.body).toHaveProperty("pdf");
expect(response.body.pdf).toHaveProperty("generated");
});
test("POST /buy/:eventId should fail for non-existent event", async () => {
const response = await request(server)
.post("/buy/99999")
.set("Content-Type", "application/json")
.expect(404);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty("message", "Event not found");
});
test("POST /buy/:eventId should fail when no tickets available", async () => {
// Purchase all available tickets first
for (let i = 0; i < 5; i++) {
await request(server)
.post(`/buy/${testEventId}`)
.set("Content-Type", "application/json")
.expect(200);
}
// Try to purchase one more
const response = await request(server)
.post(`/buy/${testEventId}`)
.set("Content-Type", "application/json")
.expect(409);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty(
"message",
"No tickets available for this event"
);
});
test("POST /buy/:eventId should validate event ID format", async () => {
const response = await request(server)
.post("/buy/invalid")
.set("Content-Type", "application/json")
.expect(400);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty("message", "Invalid event ID");
});
});
describe("Ticket Download Endpoint", () => {
let purchaseId;
beforeEach(async () => {
// Purchase a ticket first
const response = await request(server)
.post(`/buy/${testEventId}`)
.set("Content-Type", "application/json");
purchaseId = response.body.purchaseId;
});
test("GET /tickets/:purchaseId should download ticket PDF", async () => {
const response = await request(server)
.get(`/tickets/${purchaseId}`)
.expect(200);
expect(response.headers["content-type"]).toBe("application/pdf");
expect(response.headers["content-disposition"]).toContain(
`filename="ticket-${purchaseId}.pdf"`
);
expect(response.body).toBeDefined();
});
test("GET /tickets/:purchaseId should return 404 for non-existent ticket", async () => {
const response = await request(server)
.get("/tickets/non-existent-uuid")
.expect(404);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty("message", "Ticket not found");
});
test("GET /tickets/:purchaseId should validate purchase ID format", async () => {
const response = await request(server)
.get("/tickets/invalid-uuid")
.expect(400);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty("message", "Invalid purchase ID");
});
});
describe("Admin Endpoints", () => {
test("GET /admin/pdf-stats should return PDF statistics", async () => {
const response = await request(server)
.get("/admin/pdf-stats")
.expect(200);
expect(response.body).toHaveProperty("success", true);
expect(response.body).toHaveProperty("stats");
expect(response.body.stats).toHaveProperty("totalFiles");
expect(response.body.stats).toHaveProperty("totalSize");
});
test("POST /admin/cleanup-tickets should cleanup old tickets", async () => {
const response = await request(server)
.post("/admin/cleanup-tickets")
.set("Content-Type", "application/json")
.send({ maxAgeHours: 24 })
.expect(200);
expect(response.body).toHaveProperty("success", true);
expect(response.body).toHaveProperty("message");
expect(response.body).toHaveProperty("deletedCount");
});
test("POST /admin/cleanup-tickets should validate maxAgeHours parameter", async () => {
const response = await request(server)
.post("/admin/cleanup-tickets")
.set("Content-Type", "application/json")
.send({ maxAgeHours: -1 })
.expect(400);
expect(response.body).toHaveProperty("success", false);
expect(response.body).toHaveProperty(
"message",
"Invalid cleanup parameters"
);
});
test("POST /admin/seed-fallback should seed fallback store", async () => {
const response = await request(server)
.post("/admin/seed-fallback")
.set("Content-Type", "application/json")
.expect(200);
expect(response.body).toHaveProperty("success", true);
expect(response.body).toHaveProperty("message");
expect(response.body).toHaveProperty("eventsCount");
expect(response.body).toHaveProperty("totalTickets");
expect(response.body).toHaveProperty("totalSold");
});
});
describe("Metrics Endpoint", () => {
test("GET /metrics should return system metrics", async () => {
const response = await request(server).get("/metrics").expect(200);
expect(response.body).toHaveProperty("timestamp");
expect(response.body).toHaveProperty("global");
expect(response.body).toHaveProperty("events");
expect(response.body).toHaveProperty("system");
expect(response.body).toHaveProperty("pdf");
expect(response.body.system).toHaveProperty("usingFallback");
expect(response.body.system).toHaveProperty("redisConnected");
expect(response.body.system).toHaveProperty("uptime");
expect(response.body.system).toHaveProperty("memoryUsage");
});
});
describe("Fallback Mode Operation", () => {
test("should operate in fallback mode when Redis is unavailable", async () => {
// Disconnect Redis to simulate failure
if (redisClient.isConnected) {
await redisClient.disconnect();
}
// Seed fallback store
const metadata = {
eventId: testEventId,
name: "Test Event for Fallback Testing",
description: "Test event in fallback mode",
totalTickets: 3,
soldTickets: 0,
createdAt: new Date().toISOString(),
lastSoldAt: "never",
};
const testTickets = [
"fallback-ticket-1",
"fallback-ticket-2",
"fallback-ticket-3",
];
fallbackStore.seedEvent(testEventId, testTickets, metadata);
fallbackStore.activate("Test fallback mode");
// Test events endpoint in fallback mode
const eventsResponse = await request(server).get("/events").expect(200);
expect(eventsResponse.body.usingFallback).toBe(true);
// Test ticket purchase in fallback mode
const purchaseResponse = await request(server)
.post(`/buy/${testEventId}`)
.set("Content-Type", "application/json")
.expect(200);
expect(purchaseResponse.body.success).toBe(true);
expect(purchaseResponse.body.usingFallback).toBe(true);
expect(purchaseResponse.body.ticket).toBeDefined();
// Verify ticket was removed from fallback store
const event = fallbackStore.events.get(testEventId);
expect(event.tickets).toHaveLength(2);
});
});
describe("Error Handling", () => {
test("should handle malformed JSON requests gracefully", async () => {
const response = await request(server)
.post(`/buy/${testEventId}`)
.set("Content-Type", "application/json")
.send("invalid json")
.expect(400);
expect(response.body).toHaveProperty("success", false);
});
test("should handle missing required parameters", async () => {
const response = await request(server)
.post("/admin/cleanup-tickets")
.set("Content-Type", "application/json")
.send({})
.expect(200); // Should use default value
expect(response.body).toHaveProperty("success", true);
});
});
});
@@ -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);
});
});
});
+416
View File
@@ -0,0 +1,416 @@
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);
});
});
});
});
+48
View File
@@ -0,0 +1,48 @@
// Test setup and configuration
process.env.NODE_ENV = "test";
process.env.PORT = "0"; // Use random port for tests
process.env.REDIS_URL = "redis://localhost:6379";
process.env.LOG_LEVEL = "error"; // Reduce log noise during tests
process.env.PDF_OUTPUT_DIR = "test-tickets";
// Global test utilities
global.testUtils = {
// Generate unique test IDs
generateTestId: () =>
`test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
// Wait for a specified time
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
// Generate test event data
createTestEvent: (eventId = 1, ticketCount = 100) => ({
eventId: eventId.toString(),
name: `Test Event ${eventId}`,
description: `Test event description ${eventId}`,
totalTickets: ticketCount,
soldTickets: 0,
remainingTickets: ticketCount,
createdAt: new Date().toISOString(),
lastSoldAt: "never",
}),
// Generate test ticket data
createTestTicket: (eventId = 1, ticketId = "test-ticket-1") => ({
ticketId,
eventId: eventId.toString(),
purchaseId: `test-purchase-${Date.now()}`,
timestamp: new Date().toISOString(),
}),
};
// Mock console methods to reduce noise during tests
global.console = {
...console,
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
// Increase timeout for integration tests
jest.setTimeout(30000);
+240
View File
@@ -0,0 +1,240 @@
const fallbackStore = require("../../src/utils/fallback-store");
describe("Fallback Store", () => {
beforeEach(() => {
// Reset fallback store state before each test
fallbackStore.deactivate();
fallbackStore.events.clear();
fallbackStore.globalStats = {
totalEvents: 0,
totalTickets: 0,
totalSold: 0,
lastSeeded: null,
};
});
describe("Initialization", () => {
test("should start in inactive state", () => {
expect(fallbackStore.isActive).toBe(false);
expect(fallbackStore.events.size).toBe(0);
});
test("should have empty global stats", () => {
const stats = fallbackStore.getGlobalStats();
expect(stats.totalEvents).toBe(0);
expect(stats.totalTickets).toBe(0);
expect(stats.totalSold).toBe(0);
});
});
describe("Activation/Deactivation", () => {
test("should activate and deactivate correctly", () => {
fallbackStore.activate("Test activation");
expect(fallbackStore.isActive).toBe(true);
expect(fallbackStore.activationReason).toBe("Test activation");
fallbackStore.deactivate();
expect(fallbackStore.isActive).toBe(false);
expect(fallbackStore.activationReason).toBe(null);
});
test("should log activation and deactivation", () => {
const consoleSpy = jest.spyOn(console, "warn");
fallbackStore.activate("Test");
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Fallback store activated")
);
fallbackStore.deactivate();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Fallback store deactivated")
);
});
});
describe("Event Seeding", () => {
test("should seed events correctly", () => {
const eventId = "1";
const tickets = ["ticket1", "ticket2", "ticket3"];
const metadata = {
eventId: "1",
name: "Test Event",
description: "Test Description",
totalTickets: 3,
soldTickets: 0,
createdAt: "2024-01-01T00:00:00.000Z",
lastSoldAt: "never",
};
fallbackStore.seedEvent(eventId, tickets, metadata);
expect(fallbackStore.events.has(eventId)).toBe(true);
const event = fallbackStore.events.get(eventId);
expect(event.tickets).toEqual(tickets);
expect(event.metadata).toEqual(metadata);
expect(fallbackStore.globalStats.totalEvents).toBe(1);
expect(fallbackStore.globalStats.totalTickets).toBe(3);
});
test("should update global stats when seeding multiple events", () => {
const event1 = { eventId: "1", totalTickets: 5 };
const event2 = { eventId: "2", totalTickets: 3 };
fallbackStore.seedEvent("1", ["t1", "t2", "t3", "t4", "t5"], event1);
fallbackStore.seedEvent("2", ["t6", "t7", "t8"], event2);
expect(fallbackStore.globalStats.totalEvents).toBe(2);
expect(fallbackStore.globalStats.totalTickets).toBe(8);
});
});
describe("Ticket Purchase", () => {
beforeEach(() => {
// Seed a test event
const metadata = {
eventId: "1",
name: "Test Event",
description: "Test Description",
totalTickets: 3,
soldTickets: 0,
createdAt: "2024-01-01T00:00:00.000Z",
lastSoldAt: "never",
};
fallbackStore.seedEvent("1", ["ticket1", "ticket2", "ticket3"], metadata);
});
test("should purchase ticket successfully", () => {
const purchaseId = "test-purchase-123";
const result = fallbackStore.purchaseTicket("1", purchaseId);
expect(result.success).toBe(true);
expect(result.ticket).toBeDefined();
expect(result.soldCount).toBe(1);
expect(result.remainingTickets).toBe(2);
// Verify ticket was removed
const event = fallbackStore.events.get("1");
expect(event.tickets).toHaveLength(2);
expect(event.tickets).not.toContain(result.ticket);
});
test("should prevent duplicate ticket sales", () => {
const purchaseId1 = "test-purchase-1";
const purchaseId2 = "test-purchase-2";
// First purchase should succeed
const result1 = fallbackStore.purchaseTicket("1", purchaseId1);
expect(result1.success).toBe(true);
// Second purchase should succeed (different purchase ID)
const result2 = fallbackStore.purchaseTicket("1", purchaseId2);
expect(result2.success).toBe(true);
// Verify different tickets were sold
expect(result1.ticket).not.toBe(result2.ticket);
expect(result1.ticket).toBeDefined();
expect(result2.ticket).toBeDefined();
});
test("should fail when no tickets available", () => {
// Purchase all available tickets
fallbackStore.purchaseTicket("1", "purchase1");
fallbackStore.purchaseTicket("1", "purchase2");
fallbackStore.purchaseTicket("1", "purchase3");
// Try to purchase when no tickets left
const result = fallbackStore.purchaseTicket("1", "purchase4");
expect(result.success).toBe(false);
expect(result.error).toBe("NO_TICKETS_AVAILABLE");
});
test("should fail for non-existent event", () => {
const result = fallbackStore.purchaseTicket("999", "test-purchase");
expect(result.success).toBe(false);
expect(result.error).toBe("EVENT_NOT_FOUND");
});
});
describe("Event Statistics", () => {
beforeEach(() => {
const metadata = {
eventId: "1",
name: "Test Event",
description: "Test Description",
totalTickets: 5,
soldTickets: 0,
createdAt: "2024-01-01T00:00:00.000Z",
lastSoldAt: "never",
};
fallbackStore.seedEvent("1", ["t1", "t2", "t3", "t4", "t5"], metadata);
});
test("should return correct event stats", () => {
const stats = fallbackStore.getEventStats("1");
expect(stats).toBeDefined();
expect(stats.eventId).toBe("1");
expect(stats.name).toBe("Test Event");
expect(stats.totalTickets).toBe(5);
expect(stats.remainingTickets).toBe(5);
expect(stats.soldTickets).toBe(0);
});
test("should return null for non-existent event", () => {
const stats = fallbackStore.getEventStats("999");
expect(stats).toBeNull();
});
test("should update stats after ticket purchase", () => {
fallbackStore.purchaseTicket("1", "test-purchase");
const stats = fallbackStore.getEventStats("1");
expect(stats.remainingTickets).toBe(4);
expect(stats.soldTickets).toBe(1);
});
});
describe("Global Statistics", () => {
test("should return correct global stats", () => {
const stats = fallbackStore.getGlobalStats();
expect(stats.totalEvents).toBe(0);
expect(stats.totalTickets).toBe(0);
expect(stats.totalSold).toBe(0);
expect(stats.lastSeeded).toBeNull();
});
test("should update global stats after seeding", () => {
const metadata = {
eventId: "1",
name: "Test Event",
totalTickets: 3,
soldTickets: 0,
createdAt: "2024-01-01T00:00:00.000Z",
lastSoldAt: "never",
};
fallbackStore.seedEvent("1", ["t1", "t2", "t3"], metadata);
const stats = fallbackStore.getGlobalStats();
expect(stats.totalEvents).toBe(1);
expect(stats.totalTickets).toBe(3);
});
});
describe("Status Information", () => {
test("should return correct status", () => {
const status = fallbackStore.getStatus();
expect(status.active).toBe(false);
expect(status.eventsCount).toBe(0);
expect(status.totalTickets).toBe(0);
expect(status.totalSold).toBe(0);
expect(status.activationReason).toBeNull();
});
test("should return correct status when active", () => {
fallbackStore.activate("Test reason");
const status = fallbackStore.getStatus();
expect(status.active).toBe(true);
expect(status.activationReason).toBe("Test reason");
});
});
});
+281
View File
@@ -0,0 +1,281 @@
const security = require("../../src/utils/security");
describe("Security Middleware", () => {
let mockReq;
let mockRes;
let mockNext;
beforeEach(() => {
mockReq = {
method: "GET",
path: "/test",
headers: {},
params: {},
body: {},
ip: "127.0.0.1",
connection: { remoteAddress: "127.0.0.1" },
socket: { remoteAddress: "127.0.0.1" },
get: jest.fn(),
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
setHeader: jest.fn(),
};
mockNext = jest.fn();
});
describe("Rate Limiters", () => {
test("should create general rate limiter", () => {
expect(security.generalLimiter).toBeDefined();
expect(typeof security.generalLimiter).toBe("function");
});
test("should create purchase rate limiter", () => {
expect(security.purchaseLimiter).toBeDefined();
expect(typeof security.purchaseLimiter).toBe("function");
});
test("should create admin rate limiter", () => {
expect(security.adminLimiter).toBeDefined();
expect(typeof security.adminLimiter).toBe("function");
});
});
describe("Input Validation", () => {
describe("validateEventId", () => {
test("should pass valid event ID", () => {
mockReq.params.eventId = "123";
security.validateEventId[0](mockReq, mockRes, mockNext);
security.validateEventId[1](mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(mockReq.params.eventId).toBe(123); // Should be converted to number
});
test("should reject invalid event ID", () => {
mockReq.params.eventId = "invalid";
security.validateEventId[0](mockReq, mockRes, mockNext);
security.validateEventId[1](mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({
success: false,
message: "Invalid event ID",
errors: expect.any(Array),
});
});
test("should reject negative event ID", () => {
mockReq.params.eventId = "-1";
security.validateEventId[0](mockReq, mockRes, mockNext);
security.validateEventId[1](mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(400);
});
test("should reject zero event ID", () => {
mockReq.params.eventId = "0";
security.validateEventId[0](mockReq, mockRes, mockNext);
security.validateEventId[1](mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(400);
});
});
describe("validatePurchaseId", () => {
test("should pass valid UUID", () => {
mockReq.params.purchaseId = "123e4567-e89b-12d3-a456-426614174000";
security.validatePurchaseId[0](mockReq, mockRes, mockNext);
security.validatePurchaseId[1](mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});
test("should reject invalid UUID", () => {
mockReq.params.purchaseId = "invalid-uuid";
security.validatePurchaseId[0](mockReq, mockRes, mockNext);
security.validatePurchaseId[1](mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({
success: false,
message: "Invalid purchase ID",
errors: expect.any(Array),
});
});
});
describe("validateCleanupRequest", () => {
test("should pass valid maxAgeHours", () => {
mockReq.body.maxAgeHours = "48";
security.validateCleanupRequest[0](mockReq, mockRes, mockNext);
security.validateCleanupRequest[1](mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(mockReq.body.maxAgeHours).toBe(48); // Should be converted to number
});
test("should pass without maxAgeHours", () => {
delete mockReq.body.maxAgeHours;
security.validateCleanupRequest[0](mockReq, mockRes, mockNext);
security.validateCleanupRequest[1](mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});
test("should reject invalid maxAgeHours", () => {
mockReq.body.maxAgeHours = "99999"; // Too high
security.validateCleanupRequest[0](mockReq, mockRes, mockNext);
security.validateCleanupRequest[1](mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(400);
});
test("should reject negative maxAgeHours", () => {
mockReq.body.maxAgeHours = "-1";
security.validateCleanupRequest[0](mockReq, mockRes, mockNext);
security.validateCleanupRequest[1](mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(400);
});
});
});
describe("Security Headers", () => {
test("should be defined", () => {
expect(security.securityHeaders).toBeDefined();
expect(typeof security.securityHeaders).toBe("function");
});
});
describe("Request Size Limit", () => {
test("should pass requests within size limit", () => {
mockReq.headers["content-length"] = "1024"; // 1KB
security.requestSizeLimit(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});
test("should reject requests exceeding size limit", () => {
mockReq.headers["content-length"] = "2097152"; // 2MB
security.requestSizeLimit(mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(413);
expect(mockRes.json).toHaveBeenCalledWith({
success: false,
message: "Request entity too large. Maximum size is 1MB.",
});
});
test("should handle missing content-length", () => {
delete mockReq.headers["content-length"];
security.requestSizeLimit(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});
});
describe("CORS Options", () => {
test("should have correct structure", () => {
expect(security.corsOptions).toBeDefined();
expect(security.corsOptions.origin).toBeDefined();
expect(security.corsOptions.methods).toBeDefined();
expect(security.corsOptions.allowedHeaders).toBeDefined();
expect(security.corsOptions.credentials).toBeDefined();
expect(security.corsOptions.maxAge).toBeDefined();
});
test("should have default origins", () => {
expect(security.corsOptions.origin).toContain("http://localhost:3000");
expect(security.corsOptions.origin).toContain("http://localhost:3049");
});
});
describe("IP Address Extraction", () => {
test("should extract IP from req.ip", () => {
mockReq.ip = "192.168.1.1";
const ip = security.getClientIP(mockReq);
expect(ip).toBe("192.168.1.1");
});
test("should fallback to connection.remoteAddress", () => {
delete mockReq.ip;
mockReq.connection.remoteAddress = "192.168.1.2";
const ip = security.getClientIP(mockReq);
expect(ip).toBe("192.168.1.2");
});
test("should fallback to socket.remoteAddress", () => {
delete mockReq.ip;
delete mockReq.connection.remoteAddress;
mockReq.socket.remoteAddress = "192.168.1.3";
const ip = security.getClientIP(mockReq);
expect(ip).toBe("192.168.1.3");
});
test("should return unknown if no IP found", () => {
delete mockReq.ip;
delete mockReq.connection.remoteAddress;
delete mockReq.socket.remoteAddress;
const ip = security.getClientIP(mockReq);
expect(ip).toBe("unknown");
});
});
describe("Security Logging", () => {
test("should log suspicious admin requests", () => {
const consoleSpy = jest.spyOn(console, "warn");
mockReq.path = "/admin/test";
security.securityLogging(mockReq, mockRes, mockNext);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Security Warning"),
expect.objectContaining({
ip: "127.0.0.1",
path: "/admin/test",
method: "GET",
})
);
expect(mockNext).toHaveBeenCalled();
});
test("should log path traversal attempts", () => {
const consoleSpy = jest.spyOn(console, "warn");
mockReq.path = "/test/../admin";
security.securityLogging(mockReq, mockRes, mockNext);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Security Warning"),
expect.objectContaining({
path: "/test/../admin",
})
);
});
test("should not log normal requests", () => {
const consoleSpy = jest.spyOn(console, "warn");
mockReq.path = "/events/1";
security.securityLogging(mockReq, mockRes, mockNext);
expect(consoleSpy).not.toHaveBeenCalled();
expect(mockNext).toHaveBeenCalled();
});
});
});