282 lines
8.7 KiB
JavaScript
282 lines
8.7 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|