diff --git a/package.json b/package.json
index e09cd4b..c4a318c 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,9 @@
"type": "commonjs",
"scripts": {
"combine:css": "node scripts/combine-css.js",
- "test": "NODE_PATH=./src mocha test/units/**/*.mjs",
+ "test": "mocha test/**/*.unit.test.js test/**/*.property.test.js",
"start": "nodemon ./src/app.js --trace-exit",
+ "debug": "nodemon --inspect-brk ./src/app.js --trace-exit",
"maildev": "maildev",
"main": "pm2 start ecosystem.config.js --only expressjs-blog-main",
"testing": "pm2 start ecosystem.config.js --only expressjs-blog-testing",
diff --git a/src/controllers/adminTokenController.js b/src/controllers/adminTokenController.js
index 3dc8f98..96ad6d7 100644
--- a/src/controllers/adminTokenController.js
+++ b/src/controllers/adminTokenController.js
@@ -13,9 +13,8 @@
if (!token) return next();
if (!validateToken(token)) {
- const error = new HttpError("Invalid or expired token", 401, { token });
- req.log.warn({ err: error, token }, "Token validation failed");
- return next();
+ const error = new SecurityEvent("INVALID_TOKEN", { token });
+ return next(error);
}
const scheme = req.protocol;
diff --git a/src/controllers/analyticsControllers.js b/src/controllers/analyticsControllers.js
index 6c8f12a..38d8da2 100644
--- a/src/controllers/analyticsControllers.js
+++ b/src/controllers/analyticsControllers.js
@@ -1,7 +1,7 @@
const db = require("../utils/sqlite3");
// Route: JavaScript-enabled tracking
-module.exports = (req, res) => {
+module.exports = (context) => (req, res) => {
const {
url = "",
referrer = "",
@@ -15,21 +15,18 @@
const directIp = req.connection.remoteAddress;
const timestamp = Date.now();
- db.run(
- `INSERT INTO analytics (timestamp, url, referrer, user_agent, viewport, load_time, event, forwardedIp, directIp, js_enabled)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- timestamp,
- url,
- referrer,
- userAgent,
- viewport,
- loadTime,
- event,
- forwardedIp,
- directIp,
- 1,
- ]
- );
+ req.log.analytics({
+ context,
+ timestamp,
+ url,
+ referrer,
+ userAgent,
+ viewport,
+ loadTime,
+ event,
+ forwardedIp,
+ directIp,
+ js_enabled: true,
+ });
res.sendStatus(204);
};
diff --git a/src/controllers/contactControllers.js b/src/controllers/contactControllers.js
index 17ca0eb..6cd81b5 100644
--- a/src/controllers/contactControllers.js
+++ b/src/controllers/contactControllers.js
@@ -1,26 +1,16 @@
// src/routes/contact.js
const { sendContactMail } = require("../utils/sendContactMail");
-
const verifyHCaptcha = require("../utils/verifyHCaptcha");
const { qualifyLink } = require("../utils/qualifyLinks");
-const {
- captureSecurityData,
- analyzeThreatLevel,
- logSecurityEvent,
-} = require("../utils/securityForensics");
+const { analyzeThreatLevel } = require("../utils/securityForensics");
const { validateAndSanitizeEmail } = require("../utils/emailValidator");
const {
isValidInput,
- handleInvalidInput,
buildSecurityData,
- logSubmission,
- handleCaptchaFailure,
- blockHighThreat,
prepareEmail,
- logSuccess,
- logUnhandledError,
-} = require("./heplers/contactHelpers");
+} = require("./helpers/contactHelpers");
+const SecurityEvent = require("#src/utils/SecurityEvent.js");
module.exports.handleContactFormPost = async (req, res, next) => {
try {
@@ -29,15 +19,21 @@
req.body.hcaptchaToken || req.body["g-recaptcha-response"];
const emailResult = validateAndSanitizeEmail(email);
+ // Validate input and handle failures
if (!isValidInput(name, subject, message, emailResult)) {
- return await handleInvalidInput(
+ const validationError = SecurityEvent.fromRequest(
req,
- next,
- { name, email, subject, message },
- emailResult
+ "VALIDATION_FAILURE",
+ {
+ formData: { name, email, subject, message },
+ failureReason: emailResult.message || "invalid_input",
+ processingStep: "validation",
+ }
);
+ return next(validationError);
}
+ // Build security context
const securityData = buildSecurityData(req, {
formData: { name, email, message, subject },
captchaProvided: !!hcaptchaToken,
@@ -45,61 +41,118 @@
step: "initial_validation",
});
+ // Analyze threat level
const threatAnalysis = analyzeThreatLevel(
{ name, email, message, subject },
securityData
);
- await logSubmission(securityData, threatAnalysis, {
- name,
- email,
- message,
- subject,
+ // Log form submission attempt
+ await SecurityEvent.logEvent("CONTACT_SUBMISSION", {
+ ...securityData,
+ threatAnalysis,
+ formData: {
+ name,
+ email,
+ hasMessage: !!message,
+ hasSubject: !!subject,
+ },
});
- if (!hcaptchaToken)
- return await handleCaptchaFailure(
- securityData,
+ // Check for CAPTCHA token
+ if (!hcaptchaToken) {
+ const captchaError = SecurityEvent.fromRequest(req, "MISSING_CAPTCHA", {
threatAnalysis,
- next,
- "missing_captcha"
- );
+ failureReason: "missing_captcha",
+ processingStep: "captcha_validation",
+ });
+ return next(captchaError);
+ }
+ // Verify CAPTCHA
const captchaValid = await verifyHCaptcha(hcaptchaToken);
- if (!captchaValid)
- return await handleCaptchaFailure(
- securityData,
+ if (!captchaValid) {
+ const captchaError = SecurityEvent.fromRequest(req, "CAPTCHA_FAILED", {
threatAnalysis,
- next,
- "captcha_failed"
- );
+ failureReason: "captcha_verification_failed",
+ processingStep: "captcha_validation",
+ });
+ return next(captchaError);
+ }
+ // Block high-threat submissions
if (threatAnalysis.level === "high") {
- await blockHighThreat(securityData, threatAnalysis);
+ await SecurityEvent.create("THREAT_BLOCKED", {
+ ...securityData,
+ threatAnalysis,
+ action: "blocked_high_threat",
+ processingStep: "threat_analysis",
+ });
+
+ // Still show success to user to avoid revealing blocking
res.customRedirect("/contact/thankyou");
return;
}
+ // Prepare and send email
const emailData = prepareEmail(
{ name, email, message, subject },
threatAnalysis
);
- await sendContactMail(emailData);
- await logSuccess(securityData, threatAnalysis);
- res.customRedirect("/contact/thankyou");
+ try {
+ await sendContactMail(emailData);
+
+ // Log successful processing
+ await SecurityEvent.logEvent("CONTACT_SUCCESS", {
+ ...securityData,
+ threatAnalysis,
+ processingResult: "success",
+ emailSent: true,
+ });
+
+ res.customRedirect("/contact/thankyou");
+ } catch (emailError) {
+ // Log email sending failure
+ const emailFailureEvent = SecurityEvent.fromRequest(
+ req,
+ "CONTACT_ERROR",
+ {
+ ...securityData,
+ threatAnalysis,
+ error: {
+ message: emailError.message,
+ stack: emailError.stack,
+ name: emailError.name,
+ },
+ processingStep: "email_sending",
+ }
+ );
+
+ return next(emailFailureEvent);
+ }
} catch (err) {
- await logUnhandledError(req, err);
- next(err);
+ // Log any unhandled errors
+ const systemError = SecurityEvent.fromRequest(req, "CONTACT_ERROR", {
+ error: {
+ message: err.message,
+ stack: err.stack,
+ name: err.name,
+ },
+ processingStep: "error_handling",
+ });
+
+ next(systemError);
}
};
-module.exports.renderContactForm = async (req, res) => {
- const securityData = captureSecurityData(req, {
- pageAccess: "contact_form",
- processingStep: "page_render",
- });
- await logSecurityEvent(securityData, "page_access");
+module.exports.renderContactForm = async (req, res) => {
+ // Log page access
+ await SecurityEvent.logAccess(req, {
+ page: "contact_form",
+ userAgent: req.get("User-Agent"),
+ referrer: req.get("Referrer"),
+ });
const context = {
csrfToken: res.locals.csrfToken,
@@ -107,15 +160,17 @@
formAction: qualifyLink("/contact"),
formMethod: "POST",
};
+
res.renderWithBaseContext("pages/contact.handlebars", context);
};
-module.exports.renderThankYouPage = async (req, res) => {
- const securityData = captureSecurityData(req, {
- pageAccess: "thankyou_page",
- processingStep: "page_render",
- });
- await logSecurityEvent(securityData, "thankyou_access");
+module.exports.renderThankYouPage = async (req, res) => {
+ // Log thank you page access
+ await SecurityEvent.logAccess(req, {
+ page: "thankyou_page",
+ userAgent: req.get("User-Agent"),
+ referrer: req.get("Referrer"),
+ });
res.renderGenericMessage({
title: "Thank You",
diff --git a/src/controllers/heplers/contactHelpers.js b/src/controllers/heplers/contactHelpers.js
index 922c36d..c44be4a 100644
--- a/src/controllers/heplers/contactHelpers.js
+++ b/src/controllers/heplers/contactHelpers.js
@@ -1,9 +1,6 @@
// src/routes/helpers/contactHelpers.js
-const HttpError = require("../../utils/HttpError");
-const {
- captureSecurityData,
- logSecurityEvent,
-} = require("../../utils/securityForensics");
+const SecurityEvent = require("#src/utils/SecurityEvent.js");
+const { captureSecurityData } = require("../../utils/securityForensics");
function isReasonableLength(str, maxLen) {
return (
@@ -19,16 +16,22 @@
isReasonableLength(message, 2000)
);
}
+
+/**
+ * Handle invalid input with consistent security logging
+ */
async function handleInvalidInput(req, next, formData, emailResult) {
- const invalidData = captureSecurityData(req, {
+ SecurityEvent.handleValidationFailure(
+ req,
formData,
- failureReason: emailResult.message || "invalid_input",
- processingStep: "validation",
- });
- await logSecurityEvent(invalidData, "validation_failure");
- next(new HttpError("Invalid input", 400, invalidData));
+ emailResult.message || "invalid_input",
+ next
+ );
}
+/**
+ * Build security data for logging
+ */
function buildSecurityData(
req,
{ formData, captchaProvided, clientData, step }
@@ -41,73 +44,66 @@
});
}
+/**
+ * Log successful form submission
+ */
async function logSubmission(securityData, threatAnalysis, formData) {
- await logSecurityEvent(
- {
- ...securityData,
- threatAnalysis,
- formData: {
- name: formData.name,
- email: formData.email,
- hasMessage: !!formData.message,
- hasSubject: !!formData.subject,
- },
+ await SecurityEvent.logEvent("CONTACT_SUCCESS", {
+ ...securityData,
+ threatAnalysis,
+ formData: {
+ name: formData.name,
+ email: formData.email,
+ hasMessage: !!formData.message,
+ hasSubject: !!formData.subject,
},
- "contact_submission"
- );
+ });
}
-async function handleCaptchaFailure(
- securityData,
- threatAnalysis,
- next,
- reason
-) {
- await logSecurityEvent(
- {
- ...securityData,
- threatAnalysis,
- validationResult: "failed",
- failureReason: reason,
- },
- "validation_failure"
- );
- next(new HttpError("Captcha verification failed", 400));
+/**
+ * Handle CAPTCHA failure with consistent logging
+ */
+async function handleCaptchaFailure(req, threatAnalysis, next, reason) {
+ SecurityEvent.handleCaptchaFailure(req, reason, threatAnalysis, next);
}
-async function blockHighThreat(securityData, threatAnalysis) {
- await logSecurityEvent(
- {
- ...securityData,
- threatAnalysis,
- action: "blocked_high_threat",
- },
- "threat_blocked"
- );
+/**
+ * Block high threat submissions
+ */
+async function blockHighThreat(req, threatAnalysis) {
+ return await SecurityEvent.blockThreat(req, threatAnalysis);
}
+/**
+ * Prepare email content with security flags if needed
+ */
function prepareEmail({ name, email, message, subject }, threatAnalysis) {
const base = { name, email, message, subject };
+
if (threatAnalysis.level === "medium") {
base.securityFlag = `[SECURITY REVIEW REQUIRED - Score: ${threatAnalysis.score}]`;
}
+
return base;
}
+/**
+ * Log successful email sending
+ */
async function logSuccess(securityData, threatAnalysis) {
- await logSecurityEvent(
- {
- ...securityData,
- threatAnalysis,
- processingResult: "success",
- emailSent: true,
- },
- "contact_success"
- );
+ await SecurityEvent.logEvent("CONTACT_SUCCESS", {
+ ...securityData,
+ threatAnalysis,
+ processingResult: "success",
+ emailSent: true,
+ });
}
+/**
+ * Log unhandled errors with security context
+ */
async function logUnhandledError(req, err) {
- const errorData = captureSecurityData(req, {
+ SecurityEvent.fromRequest(req, "CONTACT_ERROR", {
error: {
message: err.message,
stack: err.stack,
@@ -115,7 +111,6 @@
},
processingStep: "error_handling",
});
- await logSecurityEvent(errorData, "contact_error");
}
module.exports = {
diff --git a/src/controllers/secured/logsController.js b/src/controllers/secured/logsController.js
index 5faac9c..481fe3f 100644
--- a/src/controllers/secured/logsController.js
+++ b/src/controllers/secured/logsController.js
@@ -4,7 +4,17 @@
const { winstonLogger } = require("../../utils/logging");
const analyticsDb = require("../../utils/sqlite3");
-const allowedLevels = ["warn", "error", "info", "debug", "functions", "notice"];
+const allowedLevels = [
+ "warn",
+ "error",
+ "security",
+ "event",
+ "analytics",
+ "info",
+ "debug",
+ "functions",
+ "notice",
+];
const logsDbPath = path.resolve(__dirname, "../../../data/logs.sqlite3");
if (!fs.existsSync(logsDbPath)) {
diff --git a/src/middleware/adaptiveBodyParser.js b/src/middleware/adaptiveBodyParser.js
new file mode 100644
index 0000000..b189229
--- /dev/null
+++ b/src/middleware/adaptiveBodyParser.js
@@ -0,0 +1,72 @@
+// src/setupMiddleware.js
+const express = require("express");
+const { winstonLogger } = require("../utils/logging");
+
+const {
+ EXCLUDED_PATHS,
+ DATA_LIMIT_BYTES,
+ RAW_BODY_LIMIT_BYTES,
+ RAW_BODY_TYPE,
+ FALLBACK_ENCODING,
+ FALLBACK_BODY,
+} = require("../constants/middlewareConstants");
+
+// Body parsing with different limits for excluded vs normal paths
+module.exports = (req, res, next) => {
+ const isExcludedPath = EXCLUDED_PATHS.includes(req.path);
+ const limit = isExcludedPath ? RAW_BODY_LIMIT_BYTES : DATA_LIMIT_BYTES;
+
+ const contentType = req.get("content-type") || "";
+
+ if (contentType.includes("application/json")) {
+ // Parse JSON with appropriate limit
+ express.json({ limit })(req, res, (err) => {
+ if (err) {
+ winstonLogger.error("JSON parsing error:", err.message);
+ return next(err);
+ }
+ // winstonLogger.debug("Parsed JSON body:", req.body);
+ next();
+ });
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
+ // Parse form data with appropriate limit
+ express.urlencoded({ extended: false, limit })(req, res, (err) => {
+ if (err) {
+ winstonLogger.error("Form parsing error:", err.message);
+ return next(err);
+ }
+ // winstonLogger.debug("Parsed form body:", req.body);
+ next();
+ });
+ } else if (contentType.includes("multipart/form-data")) {
+ // For multipart, we'd need multer or similar, but pass through for now
+ winstonLogger.debug(
+ "Multipart form detected - may need additional handling"
+ );
+ next();
+ } else {
+ // Try form parsing first (most common for HTML forms), then JSON
+ express.urlencoded({ extended: false, limit })(req, res, (formErr) => {
+ if (formErr) {
+ winstonLogger.warn(
+ "Form parsing failed, trying JSON:",
+ formErr.message
+ );
+ express.json({ limit })(req, res, (jsonErr) => {
+ if (jsonErr) {
+ winstonLogger.error("Both parsers failed:", {
+ formErr: formErr.message,
+ jsonErr: jsonErr.message,
+ });
+ return next(jsonErr);
+ }
+ // winstonLogger.warn("Parsed JSON body (fallback):", req.body);
+ next();
+ });
+ } else {
+ // winstonLogger.debug("Parsed form body (default):", req.body);
+ next();
+ }
+ });
+ }
+};
diff --git a/src/middleware/analytics.js b/src/middleware/analytics.js
index 7548b3d..53a768c 100644
--- a/src/middleware/analytics.js
+++ b/src/middleware/analytics.js
@@ -1,19 +1,25 @@
-const db = require("../utils/sqlite3");
+// src/middleware/analytics.js
+module.exports = (context) => {
+ return (req, res, next) => {
+ if (req.method === "GET" && req.accepts("html")) {
+ const forwardedIp = req.ip;
+ const directIp = req.connection.remoteAddress;
+ const timestamp = Date.now();
+ const url = req.originalUrl;
+ const referrer = req.get("Referer") || "";
+ const userAgent = req.get("User-Agent") || "";
-module.exports = (req, res, next) => {
- if (req.method === "GET" && req.accepts("html")) {
- const forwardedIp = req.ip;
- const directIp = req.connection.remoteAddress;
- const timestamp = Date.now();
- const url = req.originalUrl;
- const referrer = req.get("Referer") || "";
- const userAgent = req.get("User-Agent") || "";
-
- db.run(
- `INSERT INTO analytics (timestamp, url, referrer, user_agent, js_enabled, forwardedIp, directIp)
- VALUES (?, ?, ?, ?, ?, ?, ?)`, // fixme, join together in main table? i dont know what i was suppose to fix. it works fine
- [timestamp, url, referrer, userAgent, 0, forwardedIp, directIp]
- );
- }
- next();
+ req.log.analytics({
+ context,
+ timestamp,
+ url,
+ referrer,
+ userAgent,
+ js_enabled: false,
+ forwardedIp,
+ directIp,
+ });
+ }
+ next();
+ };
};
diff --git a/src/middleware/authCheck.js b/src/middleware/authCheck.js
index 928b9ab..63ab9a2 100644
--- a/src/middleware/authCheck.js
+++ b/src/middleware/authCheck.js
@@ -38,9 +38,9 @@
if (SAFE_IPS.includes(clientIp)) {
req.isAuthenticated = true; // Mark as authenticated (bypassed)
if (req.log) {
- req.log.info(`Bypassing authentication for safe IP: ${clientIp}`);
+ req.log.security(`Bypassing authentication for safe IP: ${clientIp}`);
} else {
- console.info(`Bypassing authentication for safe IP: ${clientIp}`);
+ console.security(`Bypassing authentication for safe IP: ${clientIp}`);
}
return next(); // Proceed to the next middleware/route handler
}
diff --git a/src/middleware/index.js b/src/middleware/index.js
index 1767be6..686411d 100644
--- a/src/middleware/index.js
+++ b/src/middleware/index.js
@@ -1,6 +1,5 @@
// src/setupMiddleware.js
const express = require("express");
-const bodyParser = require("body-parser");
const compression = require("compression");
const routes = require("../routes");
@@ -13,95 +12,42 @@
const hbs = require("./hbs");
const authCheck = require("./authCheck");
const { redirectMiddleware } = require("./redirect");
-const { winstonLogger } = require("../utils/logging");
-const {
- TRUST_PROXY,
- EXCLUDED_PATHS,
- DATA_LIMIT_BYTES,
- RAW_BODY_LIMIT_BYTES,
- RAW_BODY_TYPE,
- FALLBACK_ENCODING,
- FALLBACK_BODY,
-} = require("../constants/middlewareConstants");
+const { TRUST_PROXY } = require("../constants/middlewareConstants");
const {
loggingMiddleware,
morganInfo,
morganWarn,
morganError,
+ morganEvent,
+ morganAnalytics,
+ morganSecurity,
} = require("./logging");
+const securedMiddleware = require("./secured");
+const securedRoutes = require("../routes/secured");
+const adaptiveBodyParser = require("./adaptiveBodyParser");
+const analytics = require("../controllers/analyticsControllers");
function setupApp() {
const app = express();
app.disable("x-powered-by");
app.set("trust proxy", TRUST_PROXY);
-
- // Body parsing with different limits for excluded vs normal paths
- app.use((req, res, next) => {
- const isExcludedPath = EXCLUDED_PATHS.includes(req.path);
- const limit = isExcludedPath ? RAW_BODY_LIMIT_BYTES : DATA_LIMIT_BYTES;
-
- const contentType = req.get("content-type") || "";
-
- if (contentType.includes("application/json")) {
- // Parse JSON with appropriate limit
- express.json({ limit })(req, res, (err) => {
- if (err) {
- winstonLogger.error("JSON parsing error:", err.message);
- return next(err);
- }
- // winstonLogger.debug("Parsed JSON body:", req.body);
- next();
- });
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
- // Parse form data with appropriate limit
- express.urlencoded({ extended: false, limit })(req, res, (err) => {
- if (err) {
- winstonLogger.error("Form parsing error:", err.message);
- return next(err);
- }
- // winstonLogger.debug("Parsed form body:", req.body);
- next();
- });
- } else if (contentType.includes("multipart/form-data")) {
- // For multipart, we'd need multer or similar, but pass through for now
- winstonLogger.debug(
- "Multipart form detected - may need additional handling"
- );
- next();
- } else {
- // Try form parsing first (most common for HTML forms), then JSON
- express.urlencoded({ extended: false, limit })(req, res, (formErr) => {
- if (formErr) {
- winstonLogger.warn(
- "Form parsing failed, trying JSON:",
- formErr.message
- );
- express.json({ limit })(req, res, (jsonErr) => {
- if (jsonErr) {
- winstonLogger.error("Both parsers failed:", {
- formErr: formErr.message,
- jsonErr: jsonErr.message,
- });
- return next(jsonErr);
- }
- // winstonLogger.warn("Parsed JSON body (fallback):", req.body);
- next();
- });
- } else {
- // winstonLogger.debug("Parsed form body (default):", req.body);
- next();
- }
- });
- }
- });
+ app.use(adaptiveBodyParser);
app.use(hbs);
// Setup logging
- app.use(logEvent, morganInfo, morganWarn, morganError, loggingMiddleware);
+ app.use(
+ morganInfo,
+ morganWarn,
+ morganError,
+ morganEvent,
+ morganAnalytics,
+ morganSecurity,
+ loggingMiddleware
+ );
app.use(authCheck);
@@ -120,7 +66,11 @@
app.use(validateRequestIntegrity);
app.use(formatHtml);
app.use(redirectMiddleware);
- app.use(routes);
+ app.post("/track", logEvent("analytics"), analytics);
+ app.post("/analytics", logEvent("analytics"), analytics);
+ app.use("/admin", logEvent("admin"), securedMiddleware, securedRoutes);
+ app.use(logEvent("public"), routes);
+
app.use(errorHandler);
return app;
diff --git a/src/middleware/logging.js b/src/middleware/logging.js
index f8ac029..7a36796 100644
--- a/src/middleware/logging.js
+++ b/src/middleware/logging.js
@@ -4,6 +4,9 @@
const morganInfo = structuredLogger("info");
const morganWarn = structuredLogger("warn");
const morganError = structuredLogger("error");
+const morganEvent = structuredLogger("event");
+const morganAnalytics = structuredLogger("analytics");
+const morganSecurity = structuredLogger("security");
// Middleware to inject logger into req
const loggingMiddleware = (req, res, next) => {
@@ -16,4 +19,7 @@
morganInfo,
morganWarn,
morganError,
+ morganEvent,
+ morganAnalytics,
+ morganSecurity,
};
diff --git a/src/routes/index.js b/src/routes/index.js
index c1d50a7..1658abc 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -3,7 +3,6 @@
const router = express.Router();
const path = require("path");
-const analytics = require("../controllers/analyticsControllers");
const robots = require("../controllers/robotsController");
const csrfToken = require("../middleware/csrfToken");
const errorPage = require("../controllers/errorPageController");
@@ -19,8 +18,6 @@
const rssFeedController = require("../controllers/rssFeedController");
const HttpError = require("../utils/HttpError");
-const securedMiddleware = require("../middleware/secured");
-const securedRoutes = require("./secured");
const stack = require("../controllers/techkStackController");
const favicon = require("serve-favicon");
@@ -31,15 +28,10 @@
res.sendStatus(200);
});
-router.use("/admin", securedMiddleware, securedRoutes);
-
router.get("/error", errorPage); // Landing page after error is logged
router.use(admin);
-router.post("/track", analytics);
-router.post("/analytics", analytics);
-
router.use(
"/static",
express.static("public", {
diff --git a/src/utils/SecurityEvent.js b/src/utils/SecurityEvent.js
new file mode 100644
index 0000000..b50023e
--- /dev/null
+++ b/src/utils/SecurityEvent.js
@@ -0,0 +1,281 @@
+// src/utils/SecurityEvent.js
+const fs = require("fs").promises;
+const path = require("path");
+const HttpError = require("./HttpError");
+const { winstonLogger } = require("./logging");
+const { captureSecurityData } = require("./securityForensics");
+
+class SecurityEvent extends HttpError {
+ constructor(eventType, metadata = {}, options = {}) {
+ // Handle both string event types and direct metadata for backwards compatibility
+ let actualEventType, actualMetadata;
+
+ if (typeof eventType === "string" && EVENT_TYPES[eventType.toUpperCase()]) {
+ actualEventType = eventType.toUpperCase();
+ actualMetadata = metadata;
+ } else if (typeof eventType === "string") {
+ // Legacy support - treat as custom event
+ actualEventType = "CUSTOM_EVENT";
+ actualMetadata = { customEventType: eventType, ...metadata };
+ } else {
+ // If first param is metadata, treat as generic security event
+ actualEventType = "SYSTEM_ERROR";
+ actualMetadata = eventType || {};
+ }
+
+ const eventConfig =
+ EVENT_TYPES[actualEventType] || EVENT_TYPES.SYSTEM_ERROR;
+
+ super(eventConfig.message, eventConfig.statusCode, actualMetadata);
+
+ this.name = "SecurityEvent";
+ this.eventType = actualEventType;
+ this.level = eventConfig.level;
+ this.category = eventConfig.category;
+ this.timestamp = new Date().toISOString();
+ this.cause = options.cause || null;
+ this.autoLog = options.autoLog !== false; // Default to true
+
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, SecurityEvent);
+ }
+
+ // Auto-log unless explicitly disabled
+ if (this.autoLog) {
+ this.log();
+ }
+ }
+
+ /**
+ * Log this security event
+ */
+ log(additionalContext = {}) {
+ const logData = {
+ eventType: this.eventType,
+ level: this.level,
+ category: this.category,
+ message: this.message,
+ timestamp: this.timestamp,
+ metadata: this.metadata,
+ ...additionalContext,
+ };
+
+ winstonLogger.security(logData);
+
+ // Handle high-threat events with special logging
+ if (
+ this.level === "critical" ||
+ this.metadata.threatAnalysis?.level === "high"
+ ) {
+ this._logHighThreatEvent(logData);
+ }
+ }
+
+ /**
+ * Create and log a security event in one call
+ */
+ static create(eventType, metadata = {}, options = {}) {
+ return new SecurityEvent(eventType, metadata, options);
+ }
+
+ /**
+ * Create and log a security event from a request context
+ */
+ static fromRequest(req, eventType, additionalData = {}, options = {}) {
+ const securityData = captureSecurityData(req, additionalData);
+ return new SecurityEvent(eventType, securityData, options);
+ }
+
+ /**
+ * Log a security event without creating an error (for success events)
+ */
+ static async logEvent(eventType, metadata = {}, additionalContext = {}) {
+ try {
+ const eventConfig =
+ EVENT_TYPES[eventType.toUpperCase()] || EVENT_TYPES.SYSTEM_ERROR;
+
+ const logEntry = {
+ eventType: eventType.toUpperCase(),
+ level: eventConfig.level,
+ category: eventConfig.category,
+ message: eventConfig.message,
+ timestamp: new Date().toISOString(),
+ metadata,
+ ...additionalContext,
+ };
+
+ winstonLogger.security(logEntry);
+
+ // Handle high-threat events
+ if (
+ eventConfig.level === "critical" ||
+ metadata.threatAnalysis?.level === "high"
+ ) {
+ await SecurityEvent._logHighThreatEvent(logEntry);
+ }
+
+ return logEntry;
+ } catch (error) {
+ winstonLogger.error(`Failed to log security event: ${error.message}`);
+ }
+ }
+
+ /**
+ * Log page access events
+ */
+ static async logAccess(req, pageData = {}, additionalData = {}) {
+ const securityData = captureSecurityData(req, {
+ pageAccess: pageData,
+ processingStep: "page_render",
+ ...additionalData,
+ });
+
+ return await SecurityEvent.logEvent("PAGE_ACCESS", securityData);
+ }
+
+ /**
+ * Create a SecurityEvent from any error
+ */
+ static fromError(
+ error,
+ eventType = "SYSTEM_ERROR",
+ additionalMetadata = {},
+ options = {}
+ ) {
+ if (error instanceof SecurityEvent) {
+ return error;
+ }
+
+ const metadata = {
+ originalError: {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ },
+ ...additionalMetadata,
+ };
+
+ return new SecurityEvent(eventType, metadata, {
+ cause: error,
+ ...options,
+ });
+ }
+
+ /**
+ * Handle validation failures with consistent logging
+ */
+ static handleValidationFailure(req, formData, reason, next) {
+ const securityData = captureSecurityData(req, {
+ formData,
+ failureReason: reason,
+ processingStep: "validation",
+ });
+
+ const securityEvent = new SecurityEvent("VALIDATION_FAILURE", securityData);
+ next(securityEvent);
+ }
+
+ /**
+ * Handle CAPTCHA failures
+ */
+ static handleCaptchaFailure(req, reason, threatAnalysis = null, next) {
+ const securityData = captureSecurityData(req, {
+ failureReason: reason,
+ threatAnalysis,
+ processingStep: "captcha_validation",
+ });
+
+ const securityEvent = new SecurityEvent("CAPTCHA_FAILED", securityData);
+ next(securityEvent);
+ }
+
+ /**
+ * Handle threat blocking
+ */
+ static async blockThreat(
+ req,
+ threatAnalysis,
+ reason = "high_threat_detected"
+ ) {
+ const securityData = captureSecurityData(req, {
+ threatAnalysis,
+ action: "blocked",
+ blockReason: reason,
+ processingStep: "threat_analysis",
+ });
+
+ return new SecurityEvent("THREAT_BLOCKED", securityData);
+ }
+
+ /**
+ * Private method to handle high-threat event logging
+ */
+ static async _logHighThreatEvent(logEntry) {
+ try {
+ const date = new Date().toISOString().split("T")[0];
+ const logDir = path.join(__dirname, "..", "..", "logs", "security");
+ await fs.mkdir(logDir, { recursive: true });
+
+ const alertFile = path.join(logDir, `high_threat_${date}.log`);
+ const message = JSON.stringify(logEntry, null, 2);
+ await fs.appendFile(alertFile, message + "\n");
+ } catch (error) {
+ winstonLogger.error(`Failed to log high-threat event: ${error.message}`);
+ }
+ }
+
+ /**
+ * Instance method for high-threat logging
+ */
+ async _logHighThreatEvent(logEntry) {
+ return SecurityEvent._logHighThreatEvent(logEntry);
+ }
+
+ /**
+ * Convert to JSON for serialization
+ */
+ toJSON() {
+ return {
+ name: this.name,
+ eventType: this.eventType,
+ level: this.level,
+ category: this.category,
+ message: this.message,
+ statusCode: this.statusCode,
+ timestamp: this.timestamp,
+ metadata: this.metadata,
+ stack: this.stack,
+ cause:
+ this.cause instanceof Error
+ ? {
+ name: this.cause.name,
+ message: this.cause.message,
+ stack: this.cause.stack,
+ }
+ : this.cause,
+ };
+ }
+
+ /**
+ * Check if this is a specific type of security event
+ */
+ isType(eventType) {
+ return this.eventType === eventType.toUpperCase();
+ }
+
+ /**
+ * Check if this is in a specific category
+ */
+ isCategory(category) {
+ return this.category === category.toLowerCase();
+ }
+
+ /**
+ * Check if this is a critical event
+ */
+ isCritical() {
+ return this.level === "critical";
+ }
+}
+
+module.exports = SecurityEvent;
diff --git a/src/utils/logging/config.js b/src/utils/logging/config.js
index 01d067e..4ed1a36 100644
--- a/src/utils/logging/config.js
+++ b/src/utils/logging/config.js
@@ -1,3 +1,4 @@
+// src/utils/logging/config.js
const path = require("path");
const customLevels = {
@@ -5,17 +6,21 @@
error: 0,
warn: 1,
security: 2,
+ event: 2,
notice: 3,
info: 4,
debug: 5,
+ analytics: 6, // use a unique value
},
colors: {
error: "red",
warn: "yellow",
security: "magenta",
+ event: "cyan",
notice: "cyan",
info: "green",
debug: "blue",
+ analytics: "gray", // or another distinct color
},
};
@@ -34,7 +39,10 @@
notice: path.join(logDir, "notice", "notice.log"),
error: path.join(logDir, "error", "error.log"),
warn: path.join(logDir, "warn", "warn.log"),
+ event: path.join(logDir, "event", "event.log"),
+ security: path.join(logDir, "security", "security.log"),
debug: path.join(logDir, "debug", "debug.log"),
+ analytics: path.join(logDir, "debug", "analytics.log"),
};
module.exports = {
diff --git a/src/utils/logging/consolePatch.js b/src/utils/logging/consolePatch.js
index d77c886..e933005 100644
--- a/src/utils/logging/consolePatch.js
+++ b/src/utils/logging/consolePatch.js
@@ -14,40 +14,40 @@
writeLog(
"INFO",
logStreams.info,
- originalConsole.log,
sessionTransport,
+ originalConsole.log,
...args
);
console.error = (...args) =>
writeLog(
"ERROR",
logStreams.error,
- originalConsole.error,
sessionTransport,
+ originalConsole.error,
...args
);
console.warn = (...args) =>
writeLog(
"WARN",
logStreams.warn,
- originalConsole.warn,
sessionTransport,
+ originalConsole.warn,
...args
);
console.info = (...args) =>
writeLog(
"INFO",
logStreams.info,
- originalConsole.info,
sessionTransport,
+ originalConsole.info,
...args
);
console.debug = (...args) =>
writeLog(
"DEBUG",
logStreams.debug,
- originalConsole.debug,
sessionTransport,
+ originalConsole.debug,
...args
);
return originalConsole;
@@ -109,14 +109,22 @@
return { timestamp, safeArgs, message, logLine };
}
-function writeLog(level, stream, consoleFn, sessionTransport, ...args) {
+function writeLog(level, stream, sessionTransport, consoleFn, ...args) {
if (!shouldLog(level)) return;
const { timestamp, safeArgs, message, logLine } = formatLog(level, ...args);
stream.write(logLine);
- sessionTransport.write({ level: level.toLowerCase(), message, timestamp });
- consoleFn(`[${timestamp}] [${level}]`, ...safeArgs);
+ if (!sessionTransport) {
+ originalConsole.warn(
+ `sessionTransport for log level '${level} is undefined`
+ );
+ } else {
+ sessionTransport.write({ level: level.toLowerCase(), message, timestamp });
+ }
+ if (consoleFn) {
+ consoleFn(`[${timestamp}] [${level}]`, ...safeArgs);
+ }
}
module.exports = {
diff --git a/src/utils/logging/index.js b/src/utils/logging/index.js
index 71a76a4..2704b3e 100644
--- a/src/utils/logging/index.js
+++ b/src/utils/logging/index.js
@@ -51,33 +51,45 @@
streams: logStreams,
function: (...args) => functionLog(functionsLogDir, ...args),
info: (...args) =>
- writeLog("INFO", logStreams.info, console.log, sessionTransport, ...args),
+ writeLog("INFO", logStreams.info, sessionTransport, console.log, ...args),
+ event: (...args) =>
+ writeLog("EVENT", logStreams.event, sessionTransport, console.log, ...args),
notice: (...args) =>
writeLog(
"NOTICE",
logStreams.notice,
- console.log,
sessionTransport,
+ console.log,
...args
),
warn: (...args) =>
- writeLog("WARN", logStreams.warn, console.warn, sessionTransport, ...args),
+ writeLog("WARN", logStreams.warn, sessionTransport, console.warn, ...args),
+ security: (...args) =>
+ writeLog(
+ "SECURITY",
+ logStreams.security,
+ sessionTransport,
+ console.warn,
+ ...args
+ ),
error: (...args) =>
writeLog(
"ERROR",
logStreams.error,
- console.error,
sessionTransport,
+ console.error,
...args
),
debug: (...args) =>
writeLog(
"DEBUG",
logStreams.debug,
- console.debug,
sessionTransport,
+ console.debug,
...args
),
+ analytics: (...args) =>
+ writeLog("ANALYTICS", logStreams.analytics, sessionTransport, ...args),
sessionInfo: () => ({
sessionId: sessionTimestamp,
sessionDir,
@@ -95,6 +107,7 @@
),
transports: [
buildTransport("info", "info"),
+ buildTransport("event", "event"),
buildTransport("error", "error"),
buildTransport("warn", "warn"),
buildTransport("debug", "debug"),
diff --git a/src/utils/logging/logger.js b/src/utils/logging/logger.js
deleted file mode 100644
index 08b5bb6..0000000
--- a/src/utils/logging/logger.js
+++ /dev/null
@@ -1,141 +0,0 @@
-const fs = require("fs");
-const path = require("path");
-const util = require("util");
-const winston = require("winston");
-const SQLiteTransport = require("../utils/SQLiteTransport");
-
-const { createLogger, format, transports } = winston;
-
-const {
- customLevels,
- LOG_LEVEL,
- logDir,
- projectRoot,
- sessionTimestamp,
- sessionDir,
- logFiles,
-} = require("./config");
-
-function initializeLogDirectories(files = logFiles) {
- Object.values(files).forEach((filePath) => {
- const dir = path.dirname(filePath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
- });
-
- const functionsLogDir = path.join(logDir, "functions");
- if (!fs.existsSync(functionsLogDir)) {
- fs.mkdirSync(functionsLogDir, { recursive: true });
- }
- return functionsLogDir;
-}
-
-function formatFunctionName(rawPath) {
- const relative = path.relative(projectRoot, rawPath).replace(/\\/g, "/");
- return relative;
-}
-
-function formatLogMessage(functionName, args) {
- const timestamp = new Date().toISOString();
- return `[${timestamp}] ${args.join(" ")}\n`;
-}
-
-const dynamicCustomStreams = {};
-function functionLog(functionName, ...args) {
- const safeFunctionName = formatFunctionName(functionName).replace(
- /[^a-z0-9_\-]/gi,
- "_"
- );
- const message = formatLogMessage(functionName, args);
-
- if (!dynamicCustomStreams[safeFunctionName]) {
- const customFilePath = path.join(
- functionsLogDir,
- `${safeFunctionName}.log`
- );
- dynamicCustomStreams[safeFunctionName] = fs.createWriteStream(
- customFilePath,
- { flags: "a" }
- );
- }
-
- dynamicCustomStreams[safeFunctionName].write(message);
-}
-
-const functionsLogDir = initializeLogDirectories();
-const logStreams = createLogStreams(logFiles);
-const sessionTransport = createSessionTransport(sessionDir);
-const sqliteTransport = new SQLiteTransport();
-
-const manualLogger = {
- streams: logStreams,
- function: functionLog,
- info: (...args) => writeLog("INFO", logStreams.info, console.log, ...args),
- notice: (...args) =>
- writeLog("NOTICE", logStreams.notice, console.log, ...args),
- warn: (...args) => writeLog("WARN", logStreams.warn, console.warn, ...args),
- error: (...args) =>
- writeLog("ERROR", logStreams.error, console.error, ...args),
- debug: (...args) =>
- writeLog("DEBUG", logStreams.debug, console.debug, ...args),
- sessionInfo: () => ({
- sessionId: sessionTimestamp,
- sessionDir,
- startTime: new Date().toISOString(),
- }),
-};
-
-const winstonLogger = createLogger({
- levels: customLevels.levels,
- format: format.combine(
- format.timestamp(),
- format.printf(
- ({ timestamp, level, message }) => `[${timestamp}] [${level}] ${message}`
- )
- ),
- transports: [
- buildTransport("info", "info"),
- buildTransport("error", "error"),
- buildTransport("warn", "warn"),
- buildTransport("debug", "debug"),
- buildTransport("notice", "notice"),
- buildTransport("security", "security"),
- sessionTransport,
- new transports.Console({
- level: LOG_LEVEL,
- format: format.combine(
- format.colorize(),
- format.timestamp(),
- format.printf(({ timestamp, level, message, ...meta }) => {
- let stack = meta.stack || "";
- if (stack) delete meta.stack;
-
- let outputMsg;
- if (typeof message === "string") {
- outputMsg = message;
- } else {
- try {
- outputMsg = JSON.stringify(message, null, 2);
- } catch {
- outputMsg = util.inspect(message, { depth: null, colors: false });
- }
- }
-
- let metaString = "";
- if (Object.keys(meta).length > 0) {
- metaString = util.inspect(meta, { depth: null, colors: false });
- }
-
- return `[${timestamp}] [${level}] ${outputMsg}\n${stack}\n${metaString}`;
- })
- ),
- }),
- sqliteTransport,
- ],
-});
-
-module.exports = {
- manualLogger,
- winstonLogger,
-};
diff --git a/src/utils/logging/streams.js b/src/utils/logging/streams.js
index 8ffb470..09003c6 100644
--- a/src/utils/logging/streams.js
+++ b/src/utils/logging/streams.js
@@ -13,6 +13,8 @@
error: fs.createWriteStream(files.error, { flags: "a" }),
warn: fs.createWriteStream(files.warn, { flags: "a" }),
debug: fs.createWriteStream(files.debug, { flags: "a" }),
+ security: fs.createWriteStream(files.security, { flags: "a" }),
+ event: fs.createWriteStream(files.event, { flags: "a" }),
};
}
@@ -43,10 +45,9 @@
level,
format: format.combine(
format.timestamp(),
- format.printf(
- ({ timestamp, level, message }) =>
- `[${timestamp}] [${level.toUpperCase()}] ${message}`
- )
+ format.printf(({ timestamp, level, message }) => {
+ return `[${timestamp}] [${level.toUpperCase()}] ${message}`;
+ })
),
});
}
diff --git a/src/utils/securityForensics.js b/src/utils/securityForensics.js
index 27db36d..9669fe3 100644
--- a/src/utils/securityForensics.js
+++ b/src/utils/securityForensics.js
@@ -187,7 +187,7 @@
};
// Log security event at custom 'security' level
- winstonLogger.log("security", logEntry);
+ winstonLogger.security(logEntry);
// Separate high-threat log file
if (data.threatAnalysis?.level === "high") {
diff --git a/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js b/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
deleted file mode 100644
index e07bac2..0000000
--- a/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// test/validateAndSanitizeEmail.fastcheck.test.js
-const { expect } = require("chai");
-const fc = require("fast-check");
-const validator = require("validator");
-
-const {
- validateAndSanitizeEmail,
- MESSAGES,
- MAX_EMAIL_LENGTH,
-} = require("../../../../src/utils/emailValidator");
-
-describe("validateAndSanitizeEmail - fast-check property-based tests", () => {
- it("should not throw for arbitrary strings", () => {
- fc.assert(
- fc.property(fc.string(), (str) => {
- const result = validateAndSanitizeEmail(str);
- expect(result).to.have.property("valid").that.is.a("boolean");
- if (result.valid) {
- expect(result).to.have.property("email").that.is.a("string");
- expect(result.email.length).to.be.at.most(MAX_EMAIL_LENGTH);
- expect(validator.isEmail(result.email)).to.equal(true);
- } else {
- expect(result).to.have.property("message").that.is.a("string");
- expect(Object.values(MESSAGES)).to.include(result.message);
- }
- })
- );
- });
-
- it("should always return valid=true for valid, normalized, RFC-compliant email addresses", () => {
- fc.assert(
- fc.property(fc.emailAddress(), (email) => {
- const result = validateAndSanitizeEmail(email);
- expect(result.valid).to.equal(true);
- expect(result.email).to.be.a("string");
- expect(validator.isEmail(result.email)).to.equal(true);
- expect(result.email.length).to.be.at.most(MAX_EMAIL_LENGTH);
- expect(result.email.includes("..")).to.equal(false);
- expect(result.email.startsWith(".")).to.equal(false);
- expect(result.email.endsWith(".")).to.equal(false);
- })
- );
- });
-
- it("should reject emails longer than MAX_EMAIL_LENGTH", () => {
- const longLocal = "a".repeat(64);
- const longDomain = "b".repeat(MAX_EMAIL_LENGTH);
- const longEmail = `${longLocal}@${longDomain}.com`; // definitely > 320
-
- const result = validateAndSanitizeEmail(longEmail);
- expect(result.valid).to.equal(false);
- expect(result.message).to.equal(MESSAGES.TOO_LONG);
- });
-
- it("should reject strings that normalize to null", () => {
- const nonEmailInput = "invalid input string";
-
- const result = validateAndSanitizeEmail(nonEmailInput);
- if (result.valid) {
- expect(result.email).to.be.a("string");
- } else {
- expect([MESSAGES.INVALID, MESSAGES.REQUIRED]).to.include(result.message);
- }
- });
-
- it('should reject emails with ".." in them', () => {
- fc.assert(
- fc.property(fc.emailAddress(), (email) => {
- const mutated = email.replace("@", "..@");
- const result = validateAndSanitizeEmail(mutated);
- expect(result.valid).to.equal(false);
- expect(result.message).to.equal(MESSAGES.INVALID);
- })
- );
- });
-
- it('should reject emails starting or ending with "."', () => {
- fc.assert(
- fc.property(fc.emailAddress(), (email) => {
- const startDot = `.${email}`;
- const endDot = `${email}.`;
-
- const result1 = validateAndSanitizeEmail(startDot);
- const result2 = validateAndSanitizeEmail(endDot);
-
- expect(result1.valid).to.equal(false);
- expect(result2.valid).to.equal(false);
- expect(result1.message).to.equal(MESSAGES.INVALID);
- expect(result2.message).to.equal(MESSAGES.INVALID);
- })
- );
- });
-});
diff --git a/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js b/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
deleted file mode 100644
index 5c052a3..0000000
--- a/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
+++ /dev/null
@@ -1,148 +0,0 @@
-// test/utils/emailValidator/validateAndSanitizeEmail.test.js
-const { expect } = require("chai");
-const sinon = require("sinon");
-const validator = require("validator");
-
-const {
- validateAndSanitizeEmail,
- MESSAGES,
- MAX_EMAIL_LENGTH,
-} = require("../../../../src/utils/emailValidator");
-
-describe("validateAndSanitizeEmail", () => {
- afterEach(() => {
- sinon.restore();
- });
-
- it("should return REQUIRED if input is undefined", () => {
- const result = validateAndSanitizeEmail(undefined);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return REQUIRED if input is null", () => {
- const result = validateAndSanitizeEmail(null);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return REQUIRED if input is a non-string type (number)", () => {
- const result = validateAndSanitizeEmail(123);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return REQUIRED if input is an empty string", () => {
- const result = validateAndSanitizeEmail("");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return INVALID if normalized email is null (validator.normalizeEmail returns null)", () => {
- sinon.stub(validator, "normalizeEmail").returns(null);
- const result = validateAndSanitizeEmail("notanemail");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return INVALID if normalized email is not valid (validator.isEmail returns false)", () => {
- sinon.stub(validator, "normalizeEmail").returns("invalid@domain");
- sinon.stub(validator, "isEmail").returns(false);
- const result = validateAndSanitizeEmail("invalid@domain");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return TOO_LONG if email exceeds MAX_EMAIL_LENGTH", () => {
- const localPart = "a".repeat(64);
- const domain = "b".repeat(255 - localPart.length - 1); // keep it under 320 when combined
- const email = `${localPart}@${domain}.com`;
-
- const tooLongEmail = `${email}${"x".repeat(MAX_EMAIL_LENGTH - email.length + 1)}`; // force >320
-
- sinon.stub(validator, "normalizeEmail").returns(tooLongEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(tooLongEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.TOO_LONG });
- });
-
- it('should return INVALID if email contains ".."', () => {
- const badEmail = "test..dot@example.com";
- sinon.stub(validator, "normalizeEmail").returns(badEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(badEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID if email starts with "."', () => {
- const badEmail = ".start@example.com";
- sinon.stub(validator, "normalizeEmail").returns(badEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(badEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID if email ends with "."', () => {
- const badEmail = "end.@example.com";
- sinon.stub(validator, "normalizeEmail").returns(badEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(badEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return valid email if all conditions are satisfied", () => {
- const rawEmail = " John.Doe@Example.com ";
- const normalized = "john.doe@example.com";
-
- sinon.stub(validator, "normalizeEmail").callsFake((email) => {
- // simulate trimming + lowercasing + normalization
- return email === "john.doe@example.com" ? normalized : null;
- });
-
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(rawEmail);
- expect(result).to.deep.equal({ valid: true, email: normalized });
- });
-
- it('should return INVALID for email with multiple "@" characters', () => {
- const result = validateAndSanitizeEmail("john@doe@example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID for email with no "@" character', () => {
- const result = validateAndSanitizeEmail("johndoe.example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return VALID for a minimally valid email address", () => {
- const result = validateAndSanitizeEmail("a@b.co");
- expect(result.valid).to.equal(true);
- expect(result.email).to.be.a("string");
- });
-
- it("should return INVALID for email with spaces in local part", () => {
- const result = validateAndSanitizeEmail("john doe@example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID for email with space after "@"', () => {
- const result = validateAndSanitizeEmail("john@ example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return INVALID for email with quoted local part (validator accepts, you might not)", () => {
- const result = validateAndSanitizeEmail('"john.doe"@example.com');
- // Accept if validator does; reject if you disallow quotes
- // Adjust depending on your business rules
- expect(result.valid).to.equal(true);
- });
-
- it("should return INVALID for email with emoji in local part", () => {
- const result = validateAndSanitizeEmail("🧟@example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return VALID for email with subdomain", () => {
- const result = validateAndSanitizeEmail("user@sub.example.com");
- expect(result.valid).to.equal(true);
- });
-});
diff --git a/test/units/utils/logging/config.unit.test.js b/test/units/utils/logging/config.unit.test.js
deleted file mode 100644
index c8b2b0c..0000000
--- a/test/units/utils/logging/config.unit.test.js
+++ /dev/null
@@ -1,79 +0,0 @@
-// test/units/utils/logging/config.test.js
-const { expect } = require("chai");
-const fs = require("fs");
-const path = require("path");
-const proxyquire = require("proxyquire").noPreserveCache();
-
-const {
- projectRoot,
- logDir,
- sessionTimestamp,
- sessionDir,
- logFiles,
- LOG_LEVELS,
-} = require("../../../../src/utils/logging/config");
-
-describe("config.js", () => {
- it("projectRoot contains package.json", () => {
- const pkgJsonPath = path.join(projectRoot, "package.json");
- const exists = fs.existsSync(pkgJsonPath);
- expect(exists).to.equal(true, `package.json not found in ${projectRoot}`);
- });
-
- it("projectRoot matches resolved 3-levels-up path", () => {
- const expected = path.resolve(__dirname, "../../../../");
- expect(projectRoot).to.equal(expected);
- });
-
- it("logDir is within projectRoot and ends with 'logs'", () => {
- expect(logDir.startsWith(projectRoot)).to.be.true;
- expect(path.basename(logDir)).to.equal("logs");
- });
-
- it("sessionTimestamp matches expected ISO pattern with no colons or dots", () => {
- expect(sessionTimestamp).to.match(
- /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/
- );
- });
-
- it("sessionDir is built from logDir and sessionTimestamp", () => {
- const expected = path.join(logDir, "sessions", sessionTimestamp);
- expect(sessionDir).to.equal(expected);
- });
-
- it("logFiles.session points to session.log in sessionDir", () => {
- expect(logFiles.session).to.equal(path.join(sessionDir, "session.log"));
- });
-
- ["info", "notice", "error", "warn", "debug"].forEach((level) => {
- it(`logFiles.${level} points to ${level}.log in correct subdir`, () => {
- expect(logFiles[level]).to.equal(
- path.join(logDir, level, `${level}.log`)
- );
- });
- });
-
- it("LOG_LEVELS defines correct level-to-priority mapping", () => {
- expect(LOG_LEVELS).to.deep.equal({
- error: 0,
- warn: 1,
- security: 2,
- notice: 3,
- info: 4,
- debug: 5,
- });
- });
-
- it("LOG_LEVEL defaults to 'info' when process.env.LOG_LEVEL is unset", () => {
- const original = process.env.LOG_LEVEL;
- delete process.env.LOG_LEVEL;
-
- const { LOG_LEVEL } = proxyquire(
- "../../../../src/utils/logging/config",
- {}
- );
- expect(LOG_LEVEL).to.equal("info");
-
- if (original !== undefined) process.env.LOG_LEVEL = original;
- });
-});
diff --git a/test/units/utils/logging/createLogStreams.unit.test.js b/test/units/utils/logging/createLogStreams.unit.test.js
deleted file mode 100644
index bb5482c..0000000
--- a/test/units/utils/logging/createLogStreams.unit.test.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// test/units/utils/logging/createLogStreams.test.js
-const fs = require("fs");
-const path = require("path");
-const { expect } = require("chai");
-const { createLogStreams } = require("../../../../src/utils/logging");
-
-describe("createLogStreams", () => {
- const testDir = path.join(__dirname, "..", "..", "..", "..", "test", "logs");
- const files = {
- info: path.join(testDir, "info.log"),
- error: path.join(testDir, "error.log"),
- warn: path.join(testDir, "warn.log"),
- notice: path.join(testDir, "notice.log"),
- debug: path.join(testDir, "debug.log"),
- };
-
- afterEach(() => {
- Object.values(files).forEach((file) => {
- try {
- fs.unlinkSync(file);
- } catch (_) {}
- });
- });
-
- it("should create write streams for all log files", () => {
- const streams = createLogStreams(files);
- for (const key of Object.keys(files)) {
- expect(streams[key]).to.be.an.instanceof(fs.WriteStream);
- }
- });
-});
diff --git a/test/units/utils/logging/formatFunctionName.unit.test.js b/test/units/utils/logging/formatFunctionName.unit.test.js
deleted file mode 100644
index def137f..0000000
--- a/test/units/utils/logging/formatFunctionName.unit.test.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// test/units/utils/logging/formatFunctionName.test.js
-const { expect } = require("chai");
-const path = require("path");
-const { formatFunctionName } = require("../../../../src/utils/logging");
-
-describe("formatFunctionName", () => {
- it("returns relative path with forward slashes", () => {
- const base = path.join(__dirname, "..", "..", "..", "..");
- const testPath = path.join(base, "src", "somefile.js");
- const result = formatFunctionName(testPath, base);
-
- expect(result).to.equal("src/somefile.js");
- });
-});
diff --git a/test/units/utils/logging/formatLog.unit.test.js b/test/units/utils/logging/formatLog.unit.test.js
deleted file mode 100644
index c073a81..0000000
--- a/test/units/utils/logging/formatLog.unit.test.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const { formatLog } = require("../../../../src/utils/logging/consolePatch");
-const { expect } = require("chai");
-
-describe("Logger Format Function Tests", () => {
- it("should format circular objects without throwing and stringify correctly", () => {
- const circular = { name: "test" };
- circular.ref = circular;
-
- const methods = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"];
-
- methods.forEach((level) => {
- const { timestamp, safeArgs, message, logLine } = formatLog(
- level,
- circular
- );
-
- expect(timestamp).to.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/);
- expect(safeArgs).to.be.an("array");
- expect(message).to.include("name");
- expect(message).to.include("test");
- expect(message).to.not.include("[object Object]");
- expect(logLine).to.include(`[${timestamp}] [${level}]`);
- expect(logLine).to.include(message);
- });
- });
-});
diff --git a/test/units/utils/logging/formatLogMessage.unit.test.js b/test/units/utils/logging/formatLogMessage.unit.test.js
deleted file mode 100644
index 0dc0aeb..0000000
--- a/test/units/utils/logging/formatLogMessage.unit.test.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// test/units/utils/logging/formatLogMessage.test.js
-const { expect } = require("chai");
-const { formatLogMessage } = require("../../../../src/utils/logging");
-
-describe("formatLogMessage", () => {
- it("formats message with timestamp and args", () => {
- const fn = "testFunc.js";
- const args = ["arg1", "arg2"];
- const result = formatLogMessage(fn, args);
-
- expect(result).to.match(/\[\d{4}-\d{2}-\d{2}T/); // ISO date start
- expect(result).to.include("arg1 arg2");
- expect(result).to.match(/\n$/);
- });
-});
diff --git a/test/units/utils/logging/handleUncaughtException.unit.test.js b/test/units/utils/logging/handleUncaughtException.unit.test.js
deleted file mode 100644
index 6e4e3c5..0000000
--- a/test/units/utils/logging/handleUncaughtException.unit.test.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// test/units/utils/logging/handleUncaughtException.test.js
-const { expect } = require("chai");
-const sinon = require("sinon");
-const proxyquire = require("proxyquire").noCallThru();
-
-describe("handleUncaughtException", () => {
- it("logs error using winstonLogger", () => {
- const errorStub = sinon.stub();
-
- const fakeLogger = {
- winstonLogger: {
- error: errorStub,
- },
- };
-
- const { handleUncaughtException } = proxyquire(
- "../../../../src/utils/logging/handlers",
- {
- "./index": fakeLogger,
- }
- );
-
- const err = new Error("fail");
- handleUncaughtException(err);
-
- expect(errorStub.calledWith("Uncaught Exception:", err.stack)).to.be.true;
- });
-});
diff --git a/test/units/utils/logging/handleUnhandledRejection.unit.test.js b/test/units/utils/logging/handleUnhandledRejection.unit.test.js
deleted file mode 100644
index 74e720b..0000000
--- a/test/units/utils/logging/handleUnhandledRejection.unit.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const { expect } = require("chai");
-const sinon = require("sinon");
-const path = require("path");
-const proxyquire = require("proxyquire");
-
-describe("handleUnhandledRejection", () => {
- it("logs rejection using winstonLogger", () => {
- const errorStub = sinon.stub();
- const reason = new Error("rejection");
-
- const handlers = proxyquire(
- path.resolve(__dirname, "../../../../src/utils/logging/handlers"),
- {
- "../logging": { winstonLogger: { error: errorStub } },
- }
- );
-
- handlers.handleUnhandledRejection(reason);
- expect(errorStub.calledWith("Unhandled Rejection:", reason.stack)).to.be
- .true;
- });
-});
diff --git a/test/units/utils/logging/initializeLogDirectories.unit.test.js b/test/units/utils/logging/initializeLogDirectories.unit.test.js
deleted file mode 100644
index 15acc5a..0000000
--- a/test/units/utils/logging/initializeLogDirectories.unit.test.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// test/units/utils/logging/initializeLogDirectories.test.js
-const { expect } = require("chai");
-const fs = require("fs");
-const path = require("path");
-const mockFs = require("mock-fs");
-const { initializeLogDirectories } = require("../../../../src/utils/logging");
-
-describe("initializeLogDirectories", () => {
- const customLogFiles = {
- info: "../test/logs/info/info.log",
- error: "../test/logs/error/error.log",
- warn: "../test/logs/warn/warn.log",
- notice: "../test/logs/notice/notice.log",
- debug: "../test/logs/debug/debug.log",
- };
-
- afterEach(() => mockFs.restore());
-
- it("should create all required directories for given log files", () => {
- mockFs({});
- const result = initializeLogDirectories(customLogFiles);
-
- for (const file of Object.values(customLogFiles)) {
- const dir = path.dirname(file);
- expect(fs.existsSync(dir)).to.be.true;
- }
-
- expect(fs.existsSync(result)).to.be.true;
- });
-
- it("should not fail if directories already exist", () => {
- const dirs = Object.values(customLogFiles).reduce(
- (acc, file) => {
- acc[path.dirname(file)] = {};
- return acc;
- },
- { "../test/logs/functions": {} }
- );
-
- mockFs(dirs);
-
- expect(() => initializeLogDirectories(customLogFiles)).to.not.throw();
- });
-});
diff --git a/test/units/utils/logging/object-formatting.unit.test.js b/test/units/utils/logging/object-formatting.unit.test.js
deleted file mode 100644
index 0649a60..0000000
--- a/test/units/utils/logging/object-formatting.unit.test.js
+++ /dev/null
@@ -1,521 +0,0 @@
-const { expect } = require("chai");
-const sinon = require("sinon");
-const fs = require("fs");
-const path = require("path");
-const { Writable } = require("stream");
-
-// Mock dependencies
-const mockLogStreams = {
- info: new Writable({ write() {} }),
- error: new Writable({ write() {} }),
- warn: new Writable({ write() {} }),
- debug: new Writable({ write() {} }),
- notice: new Writable({ write() {} }),
-};
-
-const mockSessionTransport = {
- write: sinon.stub(),
-};
-
-// Import the modules under test
-const { writeLog } = require("../../../../src/utils/logging/consolePatch");
-const {
- manualLogger,
- winstonLogger,
-} = require("../../../../src/utils/logging/index");
-
-describe("Logger Object Expansion Tests", () => {
- let streamWriteStubs;
-
- beforeEach(() => {
- // Create fresh stream write stubs for each test
- streamWriteStubs = {
- info: sinon.stub(mockLogStreams.info, "write"),
- error: sinon.stub(mockLogStreams.error, "write"),
- warn: sinon.stub(mockLogStreams.warn, "write"),
- debug: sinon.stub(mockLogStreams.debug, "write"),
- notice: sinon.stub(mockLogStreams.notice, "write"),
- };
-
- // Reset session transport
- mockSessionTransport.write.reset();
- });
-
- afterEach(() => {
- // Restore all stubs
- sinon.restore(); // This restores all stubs created by sinon.stub()
- });
-
- describe("writeLog function", () => {
- let consoleLogStub; // Declare stub for this describe block
-
- beforeEach(() => {
- // Stub console.log specifically for this describe block
- consoleLogStub = sinon.stub(console, "log");
- });
-
- afterEach(() => {
- // Restore console.log stub after each test in this block
- consoleLogStub.restore();
- });
-
- it("should never log [object Object] for simple objects", () => {
- const testObject = { name: "test", value: 42 };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- testObject
- );
-
- // Check console output
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("name");
- expect(outputString).to.include("test");
- expect(outputString).to.include("value");
- expect(outputString).to.include("42");
-
- // Check stream output
- expect(streamWriteStubs.info.called).to.be.true;
- const streamOutput = streamWriteStubs.info.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("name");
- expect(streamOutput).to.include("test");
- });
-
- it("should properly expand nested objects", () => {
- const nestedObject = {
- user: {
- id: 123,
- profile: {
- name: "John Doe",
- settings: { theme: "dark", notifications: true },
- },
- },
- };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- nestedObject
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("John Doe");
- expect(outputString).to.include("theme");
- expect(outputString).to.include("dark");
- expect(outputString).to.include("notifications");
- });
-
- it("should handle circular references without [object Object]", () => {
- const circularObj = { name: "circular" };
- circularObj.self = circularObj;
- circularObj.nested = { parent: circularObj };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- circularObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("name");
- expect(outputString).to.include("circular");
- // Should handle circular reference gracefully
- expect(outputString).to.include("self");
- });
-
- it("should expand arrays containing objects", () => {
- const arrayWithObjects = [
- { id: 1, name: "first" },
- { id: 2, name: "second", nested: { value: "test" } },
- ];
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- arrayWithObjects
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("first");
- expect(outputString).to.include("second");
- expect(outputString).to.include("nested");
- expect(outputString).to.include("test");
- });
-
- it("should handle mixed argument types without [object Object]", () => {
- const mixedArgs = [
- "String message",
- { obj: "value" },
- 42,
- ["array", "items"],
- { deeply: { nested: { object: "here" } } },
- ];
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- ...mixedArgs
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("String message");
- expect(outputString).to.include("obj");
- expect(outputString).to.include("value");
- expect(outputString).to.include("deeply");
- expect(outputString).to.include("here");
- });
-
- it("should handle Error objects without [object Object]", () => {
- const error = new Error("Test error");
- error.customProperty = { details: "additional info" };
-
- // Stub console.error for this test (local stub, not interfering with console.log stub)
- const consoleErrorStub = sinon.stub(console, "error");
-
- writeLog(
- "ERROR",
- mockLogStreams.error,
- console.error,
- mockSessionTransport,
- error
- );
-
- expect(consoleErrorStub.called).to.be.true;
- const consoleArgs = consoleErrorStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("Test error");
-
- consoleErrorStub.restore();
- });
-
- it("should handle objects with special properties", () => {
- const specialObj = {
- toString: () => "custom toString",
- valueOf: () => 99,
- [Symbol.toStringTag]: "CustomObject",
- normalProp: "normal value",
- };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- specialObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("normalProp");
- expect(outputString).to.include("normal value");
- });
- });
-
- describe("Manual Logger Methods", () => {
- let manualLoggerStubs;
- // Removed writeLogStub as manualLogger directly interacts with console
-
- beforeEach(() => {
- // Create fresh stubs for manual logger streams if they exist
- manualLoggerStubs = {};
- if (manualLogger.streams) {
- Object.keys(manualLogger.streams).forEach((level) => {
- if (
- manualLogger.streams[level] &&
- typeof manualLogger.streams[level].write === "function"
- ) {
- if (!manualLogger.streams[level].write.isSinonProxy) {
- manualLoggerStubs[level] = sinon.stub(
- manualLogger.streams[level],
- "write"
- );
- }
- }
- });
- }
- });
-
- afterEach(() => {
- // Restore all stubs created within this describe block or its tests
- sinon.restore();
- });
-
- it("should not produce [object Object] in manualLogger.info", () => {
- const testObj = { key: "value", nested: { deep: "property" } };
-
- // Stub console.log locally for this specific test
- const consoleLogStub = sinon.stub(console, "log");
- manualLogger.info(testObj);
-
- expect(consoleLogStub.called).to.be.true; // Check if console.log was called
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" "); // Join them to check content
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("key");
- expect(outputString).to.include("nested");
- expect(outputString).to.include("deep");
- consoleLogStub.restore(); // Restore after test
- });
-
- it("should not produce [object Object] in manualLogger.error", () => {
- const errorObj = {
- error: "Something went wrong",
- context: { userId: 123, action: "login" },
- };
-
- // Stub console.error for this test
- const consoleErrorStub = sinon.stub(console, "error");
-
- manualLogger.error(errorObj);
-
- expect(consoleErrorStub.called).to.be.true;
- const consoleArgs = consoleErrorStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("Something went wrong");
- expect(outputString).to.include("userId");
- expect(outputString).to.include("login");
-
- consoleErrorStub.restore();
- });
- });
-
- describe("Winston Logger", () => {
- let winstonInfoStub;
-
- beforeEach(() => {
- winstonInfoStub = sinon.stub(winstonLogger, "info");
- });
-
- afterEach(() => {
- winstonInfoStub.restore(); // Ensure winston stub is restored
- });
-
- it("should not produce [object Object] in winston logs", () => {
- const logData = {
- user: { id: 456, name: "Jane" },
- action: "update",
- metadata: { timestamp: Date.now() },
- };
-
- winstonLogger.info("User action", logData);
-
- // Check that winston was called with properly formatted data
- expect(winstonInfoStub.called).to.be.true;
- const logCall = winstonInfoStub.getCall(0).args;
- // Winston typically stringifies objects, so we check the stringified output
- const logString = JSON.stringify(logCall);
- expect(logString).to.not.include("[object Object]");
- expect(logString).to.include("Jane");
- expect(logString).to.include("update");
- });
- });
-
- describe("Edge Cases", () => {
- let consoleLogStub; // Declare stub for this describe block
-
- beforeEach(() => {
- // Stub console.log specifically for this describe block
- consoleLogStub = sinon.stub(console, "log");
- });
-
- afterEach(() => {
- // Restore console.log stub after each test in this block
- consoleLogStub.restore();
- });
-
- it("should handle null and undefined without [object Object]", () => {
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- null,
- undefined
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("null");
- expect(outputString).to.include("undefined");
- });
-
- it("should handle objects with null prototype", () => {
- const nullProtoObj = Object.create(null);
- nullProtoObj.key = "value";
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- nullProtoObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("key");
- expect(outputString).to.include("value");
- });
-
- it("should handle Date objects", () => {
- const dateObj = new Date("2023-01-01T12:00:00.000Z"); // Use ISO string for consistent output
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- dateObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- // Check for parts of the date string that are likely to be present in ISO format
- // Console.log's output for Date objects can vary, but the ISO string is often included or derived.
- expect(outputString).to.include("2023");
- // Check for the time part of the ISO string for more robustness
- expect(outputString).to.include("T12:00:00.000Z");
- });
-
- it("should handle RegExp objects", () => {
- const regexObj = /test.*pattern/gi;
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- regexObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("test");
- expect(outputString).to.include("pattern");
- expect(outputString).to.include("gi"); // Check for flags
- });
-
- it("should handle very deeply nested objects", () => {
- let deepObj = { level: 0 };
- let current = deepObj;
-
- // Create 10 levels deep
- for (let i = 1; i <= 10; i++) {
- current.next = { level: i };
- current = current.next;
- }
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- deepObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("level");
- // Check for presence of multiple levels
- expect(outputString.match(/level/g).length).to.be.at.least(10);
- });
- });
-
- describe("Stream Output Validation", () => {
- let consoleLogStub; // Declare stub for this describe block
-
- beforeEach(() => {
- // Stub console.log specifically for this describe block
- consoleLogStub = sinon.stub(console, "log");
- });
-
- afterEach(() => {
- // Restore console.log stub after each test in this block
- consoleLogStub.restore();
- });
-
- it("should ensure stream writes never contain [object Object]", () => {
- const testObjects = [
- { simple: "object" },
- { nested: { deep: { value: "test" } } },
- [{ array: "item" }],
- { mixed: ["array", { in: "object" }] },
- ];
-
- testObjects.forEach((obj, index) => {
- streamWriteStubs.info.resetHistory(); // Reset history for each iteration
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- obj
- );
-
- expect(streamWriteStubs.info.called).to.be.true;
- const streamWrites = streamWriteStubs.info.getCalls();
- streamWrites.forEach((call) => {
- const writeData = call.args[0];
- // Ensure the written data is a string and does not contain "[object Object]"
- expect(typeof writeData).to.equal("string");
- expect(writeData).to.not.include("[object Object]");
- });
- });
- });
- });
-});
diff --git a/test/units/utils/logging/writeLog.unit.test.js b/test/units/utils/logging/writeLog.unit.test.js
deleted file mode 100644
index 85070bb..0000000
--- a/test/units/utils/logging/writeLog.unit.test.js
+++ /dev/null
@@ -1,341 +0,0 @@
-// test/units/utils/logging/writeLog.test.js
-const { expect } = require("chai");
-const sinon = require("sinon");
-const { writeLog } = require("../../../../src/utils/logging/consolePatch");
-
-describe("writeLog - Object Expansion Tests", () => {
- let stream;
- let consoleFn;
- let sessionTransport;
- let clock;
- const fixedDate = new Date("2025-07-25T12:00:00.000Z");
-
- beforeEach(() => {
- stream = { write: sinon.spy() };
- consoleFn = sinon.spy();
- sessionTransport = { write: sinon.spy() };
- clock = sinon.useFakeTimers(fixedDate.getTime());
- });
-
- afterEach(() => {
- clock.restore();
- sinon.restore();
- });
-
- describe("prevents [object Object] output", () => {
- it("expands simple objects instead of showing [object Object]", () => {
- const testObject = { name: "test", value: 42 };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, testObject);
-
- const expectedTimestamp = fixedDate.toISOString();
-
- // Check stream output doesn't contain [object Object]
- expect(stream.write.called).to.be.true;
- const streamCall = stream.write.getCall(0);
- const streamOutput = streamCall.args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("name");
- expect(streamOutput).to.include("test");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("42");
-
- // Check console output doesn't contain [object Object]
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleArgs = consoleCall.args;
- expect(consoleArgs).to.exist;
- expect(Array.isArray(consoleArgs)).to.be.true;
- const consoleOutput = consoleArgs.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleArgs).to.include.members([`[${expectedTimestamp}] [INFO]`]);
-
- // Check sessionTransport message doesn't contain [object Object]
- expect(sessionTransport.write.called).to.be.true;
- const sessionCall = sessionTransport.write.getCall(0);
- const sessionData = sessionCall.args[0];
- expect(sessionData.message).to.not.include("[object Object]");
- expect(sessionData.message).to.include("name");
- expect(sessionData.message).to.include("test");
- });
-
- it("expands nested objects completely", () => {
- const nestedObject = {
- user: {
- id: 123,
- profile: {
- name: "John Doe",
- settings: { theme: "dark", notifications: true },
- },
- },
- };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, nestedObject);
-
- // Check all outputs expand the nested structure
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("John Doe");
- expect(streamOutput).to.include("theme");
- expect(streamOutput).to.include("dark");
- expect(streamOutput).to.include("notifications");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("John Doe");
- expect(consoleOutput).to.include("theme");
-
- expect(sessionTransport.write.called).to.be.true;
- const sessionMessage = sessionTransport.write.getCall(0).args[0].message;
- expect(sessionMessage).to.not.include("[object Object]");
- expect(sessionMessage).to.include("John Doe");
- });
-
- it("expands arrays containing objects", () => {
- const arrayWithObjects = [
- { id: 1, name: "first" },
- { id: 2, name: "second", nested: { value: "test" } },
- ];
-
- writeLog("INFO", stream, consoleFn, sessionTransport, arrayWithObjects);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("first");
- expect(streamOutput).to.include("second");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("test");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("first");
- expect(consoleOutput).to.include("second");
- });
-
- it("handles mixed argument types without [object Object]", () => {
- const mixedArgs = [
- "String message",
- { obj: "value" },
- 42,
- ["array", "items"],
- { deeply: { nested: { object: "here" } } },
- ];
-
- writeLog("INFO", stream, consoleFn, sessionTransport, ...mixedArgs);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("String message");
- expect(streamOutput).to.include("obj");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("deeply");
- expect(streamOutput).to.include("here");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("String message");
- expect(consoleOutput).to.include("obj");
- });
-
- it("expands Error objects properly", () => {
- const error = new Error("Test error");
- error.customProperty = { details: "additional info" };
-
- writeLog("ERROR", stream, consoleFn, sessionTransport, error);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("Test error");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("Test error");
- });
-
- it("handles objects with special properties", () => {
- const specialObj = {
- toString: () => "custom toString",
- valueOf: () => 99,
- normalProp: "normal value",
- anotherProp: { nested: "data" },
- };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, specialObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("normalProp");
- expect(streamOutput).to.include("normal value");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("data");
-
- expect(sessionTransport.write.called).to.be.true;
- const sessionMessage = sessionTransport.write.getCall(0).args[0].message;
- expect(sessionMessage).to.not.include("[object Object]");
- expect(sessionMessage).to.include("normalProp");
- });
- });
-
- describe("edge cases", () => {
- it("handles null and undefined without [object Object]", () => {
- writeLog("INFO", stream, consoleFn, sessionTransport, null, undefined);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("null");
- expect(streamOutput).to.include("undefined");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- });
-
- it("handles Date objects", () => {
- const dateObj = new Date("2023-01-01");
-
- writeLog("INFO", stream, consoleFn, sessionTransport, dateObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("2023");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- });
-
- it("handles RegExp objects", () => {
- const regexObj = /test.*pattern/gi;
-
- writeLog("INFO", stream, consoleFn, sessionTransport, regexObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("test");
- expect(streamOutput).to.include("pattern");
- });
-
- it("handles objects with null prototype", () => {
- const nullProtoObj = Object.create(null);
- nullProtoObj.key = "value";
- nullProtoObj.nested = { prop: "data" };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, nullProtoObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("key");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("prop");
- expect(streamOutput).to.include("data");
- });
-
- it("handles very deeply nested objects", () => {
- let deepObj = { level: 0 };
- let current = deepObj;
-
- // Create 5 levels deep (reasonable for testing)
- for (let i = 1; i <= 5; i++) {
- current.next = { level: i, data: `level${i}data` };
- current = current.next;
- }
-
- writeLog("INFO", stream, consoleFn, sessionTransport, deepObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("level");
- expect(streamOutput).to.include("level5data");
- });
- });
-
- describe("different log levels", () => {
- const levels = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"];
-
- levels.forEach((level) => {
- it(`expands objects properly for ${level} level`, () => {
- const testObj = {
- level: level.toLowerCase(),
- data: { nested: "value" },
- array: [{ item: "test" }],
- };
-
- writeLog(level, stream, consoleFn, sessionTransport, testObj);
-
- // Only check if the function was called for levels that should log
- if (stream.write.called) {
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("item");
- expect(streamOutput).to.include("test");
- }
-
- if (sessionTransport.write.called) {
- const sessionMessage =
- sessionTransport.write.getCall(0).args[0].message;
- expect(sessionMessage).to.not.include("[object Object]");
- expect(sessionMessage).to.include("nested");
- }
- });
- });
- });
-
- describe("multiple objects in single call", () => {
- it("expands all objects in arguments", () => {
- const obj1 = { first: "object", nested: { value: 1 } };
- const obj2 = { second: "object", array: [{ item: "test" }] };
- const obj3 = { third: { deeply: { nested: "value" } } };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, obj1, obj2, obj3);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("first");
- expect(streamOutput).to.include("second");
- expect(streamOutput).to.include("third");
- expect(streamOutput).to.include("deeply");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("item");
- expect(streamOutput).to.include("test");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("first");
- expect(consoleOutput).to.include("second");
- expect(consoleOutput).to.include("third");
- });
- });
-});
diff --git a/test/units/utils/sendContactMail/sanitizeInput.property.test.js b/test/units/utils/sendContactMail/sanitizeInput.property.test.js
deleted file mode 100644
index 3620788..0000000
--- a/test/units/utils/sendContactMail/sanitizeInput.property.test.js
+++ /dev/null
@@ -1,175 +0,0 @@
-const { expect } = require("chai");
-const fc = require("fast-check");
-const { sanitizeInput } = require("../../../../src/utils/sendContactMail");
-
-describe("sanitizeInput", () => {
- it("should remove all newline, carriage return, and angle brackets", () => {
- fc.assert(
- fc.property(fc.string(), (str) => {
- const result = sanitizeInput(str);
- expect(result).to.not.include("\r");
- expect(result).to.not.include("\n");
- expect(result).to.not.include("<");
- expect(result).to.not.include(">");
- })
- );
- });
-
- it("should return a string for any input", () => {
- fc.assert(
- fc.property(
- fc.anything(), // This can generate any value, including complex objects
- (input) => {
- const result = sanitizeInput(input);
- expect(typeof result).to.equal("string");
- }
- )
- );
- });
-
- it("should preserve safe characters when only safe characters are present", () => {
- fc.assert(
- fc.property(
- fc.string({
- // ONLY include characters that won't be filtered out
- // Explicitly exclude: \r, \n, <, >
- minLength: 0, // Allow empty string
- maxLength: 100, // Or some reasonable max length for your test
- // Generate strings of characters that are NOT \r, \n, <, or >
- // We'll filter the characters after generation
- // For individual characters, you'd typically generate a string of length 1
- // and then map/filter the characters from that string.
- // A common pattern is to use fc.array(fc.char()), but fc.char() doesn't exist.
- // So, we'll generate strings with specific character sets, or filter a broader set.
- }),
- (input) => {
- // Filter the input string to ensure it only contains allowed characters
- // This is a common pattern when specific character sets are needed
- const filteredInput = [...input]
- .filter((c) => !["\r", "\n", "<", ">"].includes(c))
- .join("");
-
- const result = sanitizeInput(filteredInput); // Pass the filtered input to your sanitize function
- expect(result).to.equal(filteredInput.trim());
- }
- )
- );
- });
-
- // You might still want a separate test for only "safe" characters
- // where it confirms that characters *not* in the removal list are preserved.
- it("should preserve characters NOT in the removal list when only such characters are present", () => {
- fc.assert(
- fc.property(
- fc.string({
- // ONLY include characters that *should not* be filtered out
- characters:
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;'\":,./?`~ \t", // Excludes \r, \n, <, >
- }),
- (input) => {
- if (/[<>\r\n]/.test(input)) fc.pre(false); // Reject inputs with excluded chars
- const result = sanitizeInput(input);
- // For inputs that *only* contain allowed characters,
- // the result should simply be the trimmed version of the input.
- expect(result).to.equal(input.trim());
- }
- )
- );
- });
-
- it("should remove carriage returns, newlines, angle brackets, and trim whitespace", () => {
- fc.assert(
- fc.property(
- fc.string({
- // Include characters that are *expected* to be removed by sanitizeInput
- // along with regular safe characters to ensure comprehensive testing.
- characters:
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !@#$%^&*()_+-=[]{}|;':\",./?`~<>\\r\\n \t", // Added spaces and tabs for trim testing
- }),
- (input) => {
- const result = sanitizeInput(input);
-
- // Manually apply the *expected* sanitization logic to create the expected output.
- // This should match precisely what sanitizeInput is designed to do.
- const expectedOutput = String(input)
- .replace(/[\r\n<>]/g, "") // Remove \r, \n, <, >
- .trim(); // Trim whitespace
-
- expect(result).to.equal(expectedOutput);
- }
- )
- );
- });
-
- it("should remove dangerous characters from any string", () => {
- fc.assert(
- fc.property(
- fc.string(), // Any string
- (input) => {
- const result = sanitizeInput(input);
-
- // Result should be a string
- expect(typeof result).to.equal("string");
-
- // Result should not contain dangerous characters
- expect(result).to.not.include("\r");
- expect(result).to.not.include("\n");
- expect(result).to.not.include("<");
- expect(result).to.not.include(">");
-
- // Result should be trimmed
- expect(result).to.equal(result.trim());
- }
- )
- );
- });
-
- it("should handle edge cases correctly", () => {
- const testCases = [
- { input: "", expected: "" },
- { input: null, expected: "null" },
- { input: undefined, expected: "undefined" },
- { input: " ", expected: "" },
- { input: "hello");
- expect(result).to.equal("scriptalert(1)/script");
- });
-
- it("trims leading and trailing spaces", () => {
- const result = sanitizeInput(" test input ");
- expect(result).to.equal("test input");
- });
-
- it("coerces non-strings to strings", () => {
- const result = sanitizeInput(12345);
- expect(result).to.equal("12345");
- });
-
- it("returns empty string for null", () => {
- const result = sanitizeInput(null);
- expect(result).to.equal("null");
- });
-
- it("returns empty string for undefined", () => {
- const result = sanitizeInput(undefined);
- expect(result).to.equal("undefined");
- });
-});
diff --git a/test/units/utils/sendContactMail/sendContactMail.property.test.js b/test/units/utils/sendContactMail/sendContactMail.property.test.js
deleted file mode 100644
index e164235..0000000
--- a/test/units/utils/sendContactMail/sendContactMail.property.test.js
+++ /dev/null
@@ -1,115 +0,0 @@
-const { expect } = require("chai");
-const sinon = require("sinon");
-const fc = require("fast-check");
-const proxyquire = require("proxyquire");
-
-describe("sendContactMail", () => {
- let sendContactMail;
- let transporterStub;
- let fsStub;
- let validateStub;
- let loggerStub;
- let HttpError;
-
- const MAIL_DOMAIN = "example.com";
- const MAIL_USER = "admin@example.com";
- const DEFAULT_SUBJECT = "Default Subject";
- const EMAIL_LOG_PATH = "/tmp/test-mail-log.json";
-
- beforeEach(() => {
- transporterStub = {
- sendMail: sinon.stub().resolves("OK"),
- };
-
- fsStub = {
- readFile: sinon.stub().resolves("[]"),
- writeFile: sinon.stub().resolves(),
- };
-
- validateStub = sinon.stub().callsFake((email) => ({
- valid: /^[^@]+@[^@]+\.[^@]+$/.test(email),
- email,
- message: "Invalid email",
- }));
-
- loggerStub = { error: sinon.stub() };
-
- HttpError = class extends Error {
- constructor(message, code) {
- super(message);
- this.code = code;
- }
- };
-
- const mod = proxyquire("../../../../src/utils/sendContactMail", {
- "./transporter": transporterStub,
- fs: { promises: fsStub },
- path: require("path"),
- "../utils/emailValidator": { validateAndSanitizeEmail: validateStub },
- "../utils/logging": { winstonLogger: loggerStub },
- "../config/emailConfig": {
- MAIL_DOMAIN,
- MAIL_USER,
- DEFAULT_SUBJECT,
- EMAIL_LOG_PATH,
- },
- "./HttpError": HttpError,
- });
-
- sendContactMail = mod.sendContactMail;
- });
-
- it("should send mail and write log for any valid email", async () => {
- await fc.assert(
- fc.asyncProperty(
- fc.record({
- name: fc.string(),
- email: fc.emailAddress(),
- subject: fc.option(fc.string(), { nil: undefined }),
- message: fc.string(),
- }),
- async (input) => {
- await sendContactMail(input);
-
- expect(transporterStub.sendMail.calledOnce).to.be.true;
- expect(fsStub.writeFile.calledOnce).to.be.true;
-
- const args = transporterStub.sendMail.firstCall.args[0];
- expect(args).to.include.keys(
- "from",
- "to",
- "replyTo",
- "subject",
- "text"
- );
-
- transporterStub.sendMail.resetHistory();
- fsStub.writeFile.resetHistory();
- }
- )
- );
- });
-
- it("should throw HttpError on invalid email", async () => {
- await fc.assert(
- fc.asyncProperty(
- fc.record({
- name: fc.string(),
- email: fc.string().filter((s) => !/^[^@]+@[^@]+\.[^@]+$/.test(s)), // force invalid
- subject: fc.string(),
- message: fc.string(),
- }),
- async (input) => {
- try {
- await sendContactMail(input);
- expect.fail("Expected HttpError");
- } catch (err) {
- expect(err).to.be.instanceOf(HttpError);
- expect(err.message).to.equal("Invalid email");
- expect(err.code).to.equal(400);
- }
- }
- )
- );
- });
-});
diff --git a/test/units/utils/sendContactMail/sendContactMail.unit.test.js b/test/units/utils/sendContactMail/sendContactMail.unit.test.js
deleted file mode 100644
index f8e3a43..0000000
--- a/test/units/utils/sendContactMail/sendContactMail.unit.test.js
+++ /dev/null
@@ -1,207 +0,0 @@
-const chai = require("chai");
-const sinon = require("sinon");
-const fs = require("fs").promises;
-const path = require("path");
-
-const chaiAsPromised =
- require("chai-as-promised").default || require("chai-as-promised");
-
-const HttpError = require("../../../../src/utils/HttpError");
-
-chai.use(chaiAsPromised);
-const { expect } = chai;
-
-describe("sendContactMail", () => {
- let validateAndSanitizeEmailStub;
- let transporterStub;
- let loggerStub;
- let sendContactMail;
- let fsReadStub;
- let fsWriteStub;
-
- const validInput = {
- name: "Jane Doe",
- email: "jane@example.com",
- subject: "Hello",
- message: "This is a test.",
- };
-
- const mockEmailResponse = {
- accepted: ["admin@example.com"],
- rejected: [],
- response: "250 Message accepted",
- envelope: {
- from: "no-reply@example.com",
- to: ["admin@example.com"],
- },
- messageId: "",
- };
-
- const mockEmailConfig = {
- MAIL_DOMAIN: "example.com",
- MAIL_USER: "admin@example.com",
- DEFAULT_SUBJECT: "New Contact Form Submission",
- EMAIL_LOG_PATH: path.join(__dirname, "../../../data/emails.json"),
- };
-
- beforeEach(() => {
- // Clear module cache
- delete require.cache[
- require.resolve("../../../../src/utils/sendContactMail")
- ];
- delete require.cache[
- require.resolve("../../../../src/utils/emailValidator")
- ];
- delete require.cache[require.resolve("../../../../src/utils/transporter")];
- delete require.cache[require.resolve("../../../../src/utils/logging")];
- delete require.cache[require.resolve("../../../../src/config/emailConfig")];
-
- // Create stubs
- validateAndSanitizeEmailStub = sinon.stub().returns({
- valid: true,
- email: "jane@example.com",
- });
-
- transporterStub = {
- sendMail: sinon.stub().resolves(mockEmailResponse),
- };
-
- loggerStub = {
- error: sinon.stub(),
- };
-
- require.cache[require.resolve("../../../../src/config/emailConfig")] = {
- exports: mockEmailConfig,
- };
-
- // Mock modules in require cache
- require.cache[require.resolve("../../../../src/utils/emailValidator")] = {
- exports: { validateAndSanitizeEmail: validateAndSanitizeEmailStub },
- };
-
- require.cache[require.resolve("../../../../src/utils/transporter")] = {
- exports: transporterStub,
- };
-
- require.cache[require.resolve("../../../../src/utils/logging")] = {
- exports: { winstonLogger: loggerStub },
- };
-
- // Set environment variables
- process.env.MAIL_DOMAIN = "example.com";
- process.env.MAIL_USER = "admin@example.com";
-
- // Create fs stubs
- fsReadStub = sinon.stub(fs, "readFile");
- fsWriteStub = sinon.stub(fs, "writeFile");
-
- // Require the module after mocking
- const module = require("../../../../src/utils/sendContactMail");
- sendContactMail = module.sendContactMail;
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("sends an email with valid input", async () => {
- fsReadStub.resolves("[]");
- fsWriteStub.resolves();
-
- const result = await sendContactMail(validInput);
-
- // Check the result matches what transporter.sendMail returns
- expect(result).to.deep.equal(mockEmailResponse);
-
- // Verify transporter.sendMail was called once
- expect(transporterStub.sendMail.calledOnce).to.be.true;
-
- // Check the email parameters
- const sendArgs = transporterStub.sendMail.getCall(0).args[0];
- expect(sendArgs.from).to.equal('"Contact Form" ');
- expect(sendArgs.to).to.equal("admin@example.com");
- expect(sendArgs.replyTo).to.equal('"Jane Doe" ');
- expect(sendArgs.subject).to.equal("Hello");
- expect(sendArgs.text).to.equal("This is a test.");
-
- // Verify file operations
- expect(fsReadStub.calledOnce).to.be.true;
- expect(fsWriteStub.calledOnce).to.be.true;
- });
-
- it("uses default subject if none provided", async () => {
- const input = { ...validInput, subject: undefined };
- fsReadStub.resolves("[]");
- fsWriteStub.resolves();
-
- const result = await sendContactMail(input);
-
- // Check that we get the mock response, not an empty object
- expect(result).to.deep.equal(mockEmailResponse);
-
- const args = transporterStub.sendMail.firstCall.args[0];
- expect(args.subject).to.equal("New Contact Form Submission");
- });
-
- it("throws HttpError on invalid email", async () => {
- validateAndSanitizeEmailStub.returns({
- valid: false,
- message: "Invalid email format",
- });
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(HttpError);
- });
-
- it("logs and rethrows on readFile error", async () => {
- const error = new Error("Disk failure");
- fsReadStub.rejects(error);
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(
- "Disk failure"
- );
-
- // Verify logger was called
- expect(loggerStub.error.calledOnce).to.be.true;
- expect(loggerStub.error.calledWith("Failed to log email to file:", error))
- .to.be.true;
- });
-
- it("throws on invalid email without message", async () => {
- validateAndSanitizeEmailStub.returns({
- valid: false,
- message: undefined,
- });
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(HttpError);
- });
-
- it("handles non-empty existing log file", async () => {
- const existingLogs = JSON.stringify([{ test: true }]);
- fsReadStub.resolves(existingLogs);
- fsWriteStub.resolves();
-
- const result = await sendContactMail(validInput);
- expect(result).to.deep.equal(mockEmailResponse);
-
- // Verify the log was written with existing data plus new entry
- expect(fsWriteStub.calledOnce).to.be.true;
- const writtenData = JSON.parse(fsWriteStub.firstCall.args[1]);
- expect(writtenData).to.be.an("array").with.lengthOf(2);
- expect(writtenData[0]).to.deep.equal({ test: true });
- });
-
- it("throws if writing log file fails", async () => {
- fsReadStub.resolves("[]");
- const error = new Error("Write failed");
- fsWriteStub.rejects(error);
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(
- "Write failed"
- );
-
- // Verify logger was called
- expect(loggerStub.error.calledOnce).to.be.true;
- expect(loggerStub.error.calledWith("Failed to log email to file:", error))
- .to.be.true;
- });
-});
diff --git a/test/units/utils/sendNewsletterSubscriptionMail.unit.test.js b/test/units/utils/sendNewsletterSubscriptionMail.unit.test.js
deleted file mode 100644
index 9a30e5d..0000000
--- a/test/units/utils/sendNewsletterSubscriptionMail.unit.test.js
+++ /dev/null
@@ -1,51 +0,0 @@
-// test/units/utils/sendNewsletterSubscriptionMail.test.js
-const sinon = require("sinon");
-const transporter = require("../../../src/utils/transporter");
-const { winstonLogger } = require("../../../src/utils/logging");
-const sendNewsletterSubscriptionMail = require("../../../src/utils/sendNewsletterSubscriptionMail");
-
-describe("sendNewsletterSubscriptionMail", () => {
- let sendMailStub;
- let errorStub;
-
- beforeEach(() => {
- process.env.MAIL_DOMAIN = "example.com";
- process.env.MAIL_NEWSLETTER = "newsletter@example.com";
-
- sendMailStub = sinon.stub(transporter, "sendMail");
- errorStub = sinon.stub(winstonLogger, "error");
- });
-
- afterEach(() => {
- sendMailStub.restore();
- errorStub.restore();
- delete process.env.MAIL_DOMAIN;
- delete process.env.MAIL_NEWSLETTER;
- });
-
- it("calls transporter.sendMail with correct mail data", async () => {
- sendMailStub.resolves("sent");
-
- const email = "user@example.com";
- const result = await sendNewsletterSubscriptionMail({ email });
-
- sinon.assert.calledOnce(sendMailStub);
- sinon.assert.calledWith(sendMailStub, {
- from: "Newsletter ",
- to: email,
- subject: "New Newsletter Subscription",
- text: "Please add this email to the newsletter list: newsletter@example.com",
- });
- sinon.assert.notCalled(errorStub);
- });
-
- it("logs error when transporter.sendMail rejects", async () => {
- const error = new Error("send failed");
- sendMailStub.rejects(error);
-
- await sendNewsletterSubscriptionMail({ email: "fail@example.com" });
-
- sinon.assert.calledOnce(errorStub);
- sinon.assert.calledWith(errorStub, error);
- });
-});
diff --git a/test/utils/logging/config.test.js b/test/utils/logging/config.test.js
new file mode 100644
index 0000000..7b6b82a
--- /dev/null
+++ b/test/utils/logging/config.test.js
@@ -0,0 +1,79 @@
+// test/units/utils/logging/config.test.js
+const { expect } = require("chai");
+const fs = require("fs");
+const path = require("path");
+const proxyquire = require("proxyquire").noPreserveCache();
+
+const {
+ projectRoot,
+ logDir,
+ sessionTimestamp,
+ sessionDir,
+ logFiles,
+ LOG_LEVELS,
+} = require("../../../src/utils/logging/config");
+
+describe("config.js", () => {
+ it("projectRoot contains package.json", () => {
+ const pkgJsonPath = path.join(projectRoot, "package.json");
+ const exists = fs.existsSync(pkgJsonPath);
+ expect(exists).to.equal(true, `package.json not found in ${projectRoot}`);
+ });
+
+ it("projectRoot matches resolved 3-levels-up path", () => {
+ const expected = path.resolve(__dirname, "../../../../");
+ expect(projectRoot).to.equal(expected);
+ });
+
+ it("logDir is within projectRoot and ends with 'logs'", () => {
+ expect(logDir.startsWith(projectRoot)).to.be.true;
+ expect(path.basename(logDir)).to.equal("logs");
+ });
+
+ it("sessionTimestamp matches expected ISO pattern with no colons or dots", () => {
+ expect(sessionTimestamp).to.match(
+ /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/
+ );
+ });
+
+ it("sessionDir is built from logDir and sessionTimestamp", () => {
+ const expected = path.join(logDir, "sessions", sessionTimestamp);
+ expect(sessionDir).to.equal(expected);
+ });
+
+ it("logFiles.session points to session.log in sessionDir", () => {
+ expect(logFiles.session).to.equal(path.join(sessionDir, "session.log"));
+ });
+
+ ["info", "notice", "error", "warn", "debug"].forEach((level) => {
+ it(`logFiles.${level} points to ${level}.log in correct subdir`, () => {
+ expect(logFiles[level]).to.equal(
+ path.join(logDir, level, `${level}.log`)
+ );
+ });
+ });
+
+ it("LOG_LEVELS defines correct level-to-priority mapping", () => {
+ expect(LOG_LEVELS).to.deep.equal({
+ error: 0,
+ warn: 1,
+ security: 2,
+ notice: 3,
+ info: 4,
+ debug: 5,
+ });
+ });
+
+ it("LOG_LEVEL defaults to 'info' when process.env.LOG_LEVEL is unset", () => {
+ const original = process.env.LOG_LEVEL;
+ delete process.env.LOG_LEVEL;
+
+ const { LOG_LEVEL } = proxyquire(
+ "../../../../src/utils/logging/config",
+ {}
+ );
+ expect(LOG_LEVEL).to.equal("info");
+
+ if (original !== undefined) process.env.LOG_LEVEL = original;
+ });
+});
diff --git a/test/utils/logging/createLogStreams.test.js b/test/utils/logging/createLogStreams.test.js
new file mode 100644
index 0000000..253578c
--- /dev/null
+++ b/test/utils/logging/createLogStreams.test.js
@@ -0,0 +1,31 @@
+// test/units/utils/logging/createLogStreams.test.js
+const fs = require("fs");
+const path = require("path");
+const { expect } = require("chai");
+const { createLogStreams } = require("../../../src/utils/logging");
+
+describe("createLogStreams", () => {
+ const testDir = path.join(__dirname, "..", "..", "..", "..", "test", "logs");
+ const files = {
+ info: path.join(testDir, "info.log"),
+ error: path.join(testDir, "error.log"),
+ warn: path.join(testDir, "warn.log"),
+ notice: path.join(testDir, "notice.log"),
+ debug: path.join(testDir, "debug.log"),
+ };
+
+ afterEach(() => {
+ Object.values(files).forEach((file) => {
+ try {
+ fs.unlinkSync(file);
+ } catch (_) {}
+ });
+ });
+
+ it("should create write streams for all log files", () => {
+ const streams = createLogStreams(files);
+ for (const key of Object.keys(files)) {
+ expect(streams[key]).to.be.an.instanceof(fs.WriteStream);
+ }
+ });
+});