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
+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();
});
});
});