'use strict'; const fs = require('fs'); const path = require('path'); const ROOT_DIR = path.resolve(__dirname, '..', '..'); const CONTROLLERS_DIR = path.join(ROOT_DIR, 'controllers'); const TESTS_DIR = __dirname; const EXCLUDED_CONTROLLERS = { export: 'Route-wiring controller module; not covered by the current Jest integration pattern.', subscription: 'Stripe-dependent controller; virtually all methods require live Stripe API and cannot run against mongodb-memory-server alone.', upload_job: 'Route-wiring controller module; not covered by the current Jest integration pattern.', }; const EXCLUDED_METHODS = { api_export: { downloadExport: 'File download path is not covered by the current integration suite.', }, client: { deleteClient: 'Skipped in Jest because the controller requires MongoDB transactions.', }, customer: { createCustomer_post: 'Skipped in Jest because the controller requires MongoDB transactions.', deleteCustomer: 'Skipped in Jest because the controller requires MongoDB transactions.', }, invoice: { createInvoice_post: 'Skipped in Jest because the controller requires MongoDB transactions.', updateInvoiceById_put: 'Skipped in Jest because the controller requires MongoDB transactions.', deleteInvoiceById: 'Skipped in Jest because the controller requires MongoDB transactions.', deleteInvoices: 'Skipped in Jest because the controller requires MongoDB transactions.', }, log_payment: { createLogPayment_post: 'Skipped in Jest because the controller requires MongoDB transactions.', createLogPayments_post: 'Skipped in Jest because the controller requires MongoDB transactions.', }, pilot: { deletePilot: 'Skipped in Jest because the controller requires MongoDB transactions.', }, user: { resetPassword_post: 'Skipped in Jest because the controller requires MongoDB transactions.', signup_post: 'Skipped in Jest because the controller requires MongoDB transactions.', ensureParentExists: 'Internal helper exported for reuse; not a request handler test target.', clearTempData: 'Internal helper exported for reuse; not a request handler test target.', getHostUrlFromReq: 'Internal helper exported for reuse; not a request handler test target.', }, vehicle: { deleteVehicle: 'Skipped in Jest because the controller requires MongoDB transactions.', }, }; function stripComments(input) { return input .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|\s)\/\/.*$/gm, '$1'); } function parseObjectMembers(block) { return stripComments(block) .split(',') .map(part => part.trim()) .filter(Boolean) .map(part => part.replace(/[}\s]+$/g, '')) .map(part => part.split(':')[0].trim()) .filter(Boolean) .filter(part => /^[A-Za-z_$][\w$]*$/.test(part)); } function parseExportedMethods(controllerSource) { const directMatch = controllerSource.match(/module\.exports\s*=\s*\{([\s\S]*?)\}\s*;?\s*$/m); if (directMatch) return parseObjectMembers(directMatch[1]); if (controllerSource.includes('module.exports = function')) { const returnMatches = [...controllerSource.matchAll(/return\s*\{([\s\S]*?)\}\s*;?/g)]; if (returnMatches.length > 0) { return parseObjectMembers(returnMatches[returnMatches.length - 1][1]); } } // Handle controllers that export via individual `exports.name = ...` assignments. const namedExportsRegex = /^exports\.([A-Za-z_$][\w$]*)\s*=/gm; const namedMatches = [...controllerSource.matchAll(namedExportsRegex)]; if (namedMatches.length > 0) { return namedMatches.map(m => m[1]); } return []; } function getCoveredMethods(testSource, methods) { const covered = new Set(); for (const method of methods) { const invocationRegex = new RegExp(`\\.${method}\\s*\\(`); if (invocationRegex.test(testSource)) covered.add(method); } return covered; } function getControllerFiles() { return fs.readdirSync(CONTROLLERS_DIR) .filter(fileName => fileName.endsWith('.js')) .sort(); } function main() { const failures = []; const warnings = []; for (const fileName of getControllerFiles()) { const controllerName = path.basename(fileName, '.js'); if (EXCLUDED_CONTROLLERS[controllerName]) { warnings.push(`SKIP ${controllerName}: ${EXCLUDED_CONTROLLERS[controllerName]}`); continue; } const controllerPath = path.join(CONTROLLERS_DIR, fileName); const testPath = path.join(TESTS_DIR, `${controllerName}.integration.test.js`); if (!fs.existsSync(testPath)) { failures.push(`Missing integration suite for controller '${controllerName}': expected tests/integration/${controllerName}.integration.test.js`); continue; } const controllerSource = fs.readFileSync(controllerPath, 'utf8'); const exportedMethods = parseExportedMethods(controllerSource); if (exportedMethods.length === 0) { failures.push(`Could not determine exported methods for controller '${controllerName}'.`); continue; } const testSource = fs.readFileSync(testPath, 'utf8'); const coveredMethods = getCoveredMethods(testSource, exportedMethods); const excludedMethods = EXCLUDED_METHODS[controllerName] || {}; for (const method of exportedMethods) { if (coveredMethods.has(method)) continue; if (excludedMethods[method]) { warnings.push(`SKIP ${controllerName}.${method}: ${excludedMethods[method]}`); continue; } failures.push( `Missing integration test coverage for ${controllerName}.${method}: add at least one test that invokes this exported method in tests/integration/${controllerName}.integration.test.js` ); } } if (warnings.length > 0) { console.log('Integration contract exclusions:'); for (const warning of warnings) console.log(`- ${warning}`); console.log(''); } if (failures.length > 0) { console.error('Integration controller contract check failed:'); for (const failure of failures) console.error(`- ${failure}`); process.exit(1); } console.log('Integration controller contract check passed.'); } main();