Source: utils/validate.js

/**
 * @module utils/validate
 * @description Lightweight input validation helpers for API routes.
 *
 * No external dependencies — keeps the backend lean. Each function returns
 * `null` if valid, or an error message string if invalid.
 *
 * @example
 * const err = validate.projectPayload(req.body);
 * if (err) return res.status(400).json({ error: err });
 */

/**
 * Trim and truncate a string. Returns empty string for non-string values.
 * @param {*} val
 * @param {number} [maxLen=500]
 * @returns {string}
 */
export function sanitise(val, maxLen = 500) {
  return typeof val === "string" ? val.trim().slice(0, maxLen) : "";
}

/**
 * Validate a URL string — must be http or https.
 * @param {string} url
 * @returns {string|null} Error message or null.
 */
export function validateUrl(url) {
  if (!url || typeof url !== "string") return "URL is required.";
  try {
    const parsed = new URL(url.trim());
    if (!["http:", "https:"].includes(parsed.protocol)) {
      return "URL must use http or https protocol.";
    }
    return null;
  } catch {
    return "Invalid URL format. Must be a valid URL (e.g. https://example.com).";
  }
}

/**
 * Validate project creation/update payload.
 * @param {Object} body - req.body
 * @returns {string|null} Error message or null.
 */
export function validateProjectPayload(body) {
  if (!body || typeof body !== "object") return "Request body is required.";
  const name = sanitise(body.name, 200);
  if (!name) return "Project name is required.";
  if (name.length < 1 || name.length > 200) return "Project name must be 1–200 characters.";
  const urlErr = validateUrl(body.url);
  if (urlErr) return urlErr;

  // Validate credentials if provided
  if (body.credentials) {
    const c = body.credentials;
    if (typeof c !== "object") return "Credentials must be an object.";
    if (c.usernameSelector && typeof c.usernameSelector !== "string") return "usernameSelector must be a string.";
    if (c.username && typeof c.username !== "string") return "username must be a string.";
    if (c.passwordSelector && typeof c.passwordSelector !== "string") return "passwordSelector must be a string.";
    if (c.password && typeof c.password !== "string") return "password must be a string.";
    if (c.submitSelector && typeof c.submitSelector !== "string") return "submitSelector must be a string.";
  }
  return null;
}

/**
 * Validate manual test creation payload.
 * @param {Object} body - req.body
 * @returns {string|null} Error message or null.
 */
export function validateTestPayload(body) {
  if (!body || typeof body !== "object") return "Request body is required.";
  const name = sanitise(body.name, 500);
  if (!name) return "Test name is required.";
  if (body.steps && !Array.isArray(body.steps)) return "Steps must be an array.";
  if (body.steps && body.steps.length > 100) return "Maximum 100 steps per test.";
  if (body.steps && body.steps.some(s => typeof s !== "string")) return "Each step must be a string.";
  if (body.priority && !["low", "medium", "high"].includes(body.priority)) return "Priority must be low, medium, or high.";
  if (body.type && typeof body.type !== "string") return "Type must be a string.";
  return null;
}

/**
 * Validate test update (PATCH) payload.
 * @param {Object} body - req.body
 * @returns {string|null} Error message or null.
 */
export function validateTestUpdate(body) {
  if (!body || typeof body !== "object") return "Request body is required.";
  if (body.name !== undefined && (typeof body.name !== "string" || !body.name.trim())) return "Name must be a non-empty string.";
  if (body.steps !== undefined && !Array.isArray(body.steps)) return "Steps must be an array.";
  if (body.steps && body.steps.length > 100) return "Maximum 100 steps per test.";
  if (body.priority !== undefined && !["low", "medium", "high"].includes(body.priority)) return "Priority must be low, medium, or high.";
  if (body.tags !== undefined && !Array.isArray(body.tags)) return "Tags must be an array.";
  if (body.tags && body.tags.length > 20) return "Maximum 20 tags per test.";
  return null;
}

/**
 * Validate bulk test action payload.
 * @param {Object} body - req.body
 * @returns {string|null} Error message or null.
 */
export function validateBulkAction(body) {
  if (!body || typeof body !== "object") return "Request body is required.";
  if (!Array.isArray(body.testIds) || body.testIds.length === 0) return "testIds must be a non-empty array.";
  if (body.testIds.length > 500) return "Maximum 500 tests per bulk action.";
  if (body.testIds.some(id => typeof id !== "string")) return "Each testId must be a string.";
  if (!["approve", "reject", "restore", "delete"].includes(body.action)) return "Action must be approve, reject, restore, or delete.";
  return null;
}