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