'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 = { dlq: 'No Jest integration suite yet.', export: 'Route-wiring controller module; not covered by the current Jest integration pattern.', geoutil: 'No Jest integration suite yet.', health: 'No Jest integration suite yet.', location: 'No Jest integration suite yet.', subscription: 'No Jest integration suite yet.', 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.', }, api_pub: { getSessionRecords: 'No Jest integration case yet.', getAreas: 'No Jest integration case yet.', }, billing: { exportUsageDetail_post: 'No Jest integration case yet.', getCustBillingStatus_get: 'No Jest integration case yet.', uploadLogo_post: 'No Jest integration case yet.', deleteLogo_delete: 'No Jest integration case yet.', }, client: { updateClient_put: 'No Jest integration case yet.', deleteClient: 'Skipped in Jest because the controller requires MongoDB transactions.', searchWithSetting_post: 'No Jest integration case yet.', }, customer: { createCustomer_post: 'No Jest integration case yet.', getCustomer_get: 'No Jest integration case yet.', updateCustomer_put: 'No Jest integration case yet.', deleteCustomer: 'No Jest integration case yet.', updateUser_put: 'No Jest integration case yet.', deleteUser: 'No Jest integration case yet.', getHierarchyById_get: 'No Jest integration case yet.', }, dealer: { updateUser_put: 'No Jest integration case yet.', deleteUser: 'No Jest integration case yet.', }, geoitem: { search_post: 'No Jest integration case yet.', getSharedItems_get: 'No Jest integration case yet.', }, invoice: { createInvoice_post: 'No Jest integration case yet.', getInvoiceById_get: 'No Jest integration case yet.', updateInvoice_put: 'No Jest integration case yet.', updateInvoiceById_put: 'No Jest integration case yet.', deleteInvoice_delete: 'No Jest integration case yet.', deleteInvoiceById: 'No Jest integration case yet.', deleteInvoices: 'No Jest integration case yet.', getInvoiceJobs_post: 'No Jest integration case yet.', sendInvoiceEmail_post: 'No Jest integration case yet.', getPreviewUrl_post: 'No Jest integration case yet.', getPrintInformation_get: 'No Jest integration case yet.', exportInvoices_get: 'No Jest integration case yet.', payment_get: 'No Jest integration case yet.', }, invoice_settings: { getInvoiceSettingDefault_get: 'No Jest integration case yet.', copyInvoiceSetting_post: 'No Jest integration case yet.', uploadLogo_post: 'No Jest integration case yet.', deleteLogo_delete: 'No Jest integration case yet.', }, job: { getData_post: 'No Jest integration case yet.', getReportOps_get: 'No Jest integration case yet.', preAppReport_post: 'No Jest integration case yet.', getRptVars_post: 'No Jest integration case yet.', setRptVars_post: 'No Jest integration case yet.', saveReport_post: 'No Jest integration case yet.', preLoadReport_post: 'No Jest integration case yet.', getUploadedFiles_post: 'No Jest integration case yet.', importStatus_post: 'No Jest integration case yet.', importingStatus_post: 'No Jest integration case yet.', deleteAppFile_post: 'No Jest integration case yet.', getJobLogs_post: 'No Jest integration case yet.', assign_post: 'No Jest integration case yet.', assignments_post: 'No Jest integration case yet.', countByClient_post: 'No Jest integration case yet.', saveMapOps_post: 'No Jest integration case yet.', appFiles_post: 'No Jest integration case yet.', filesdata_post: 'No Jest integration case yet.', getAppDataByJobId: 'No Jest integration case yet.', fetchInvReadyJobs_post: 'No Jest integration case yet.', searchJobs_post: 'No Jest integration case yet.', }, log_payment: { createLogPayment_post: 'Skipped in Jest because the controller requires MongoDB transactions.', createLogPayments_post: 'Skipped in Jest because the controller requires MongoDB transactions.', }, main: { getSiteVer_post: 'No Jest integration case yet.', doLongOp_post: 'No Jest integration case yet.', getActivePromos_get: 'No Jest integration case yet.', getSubscriptionPromos_get: 'No Jest integration case yet.', setSubscriptionPromos_post: 'No Jest integration case yet.', addSubscriptionPromo_post: 'No Jest integration case yet.', deleteSubscriptionPromo_delete: 'No Jest integration case yet.', updateSubscriptionPromo_put: 'No Jest integration case yet.', getForeverCoupons_get: 'No Jest integration case yet.', }, obstacle: { updateObstacle_put: 'No Jest integration case yet.', deleteObstacle: 'No Jest integration case yet.', }, partner: { getPartnerById_post: 'No Jest integration case yet.', updatePartner_put: 'No Jest integration case yet.', syncData_post: 'No Jest integration case yet.', uploadJob_post: 'No Jest integration case yet.', getSystemUsers_get: 'No Jest integration case yet.', getCurrentSystemUser_get: 'No Jest integration case yet.', getSystemUser_get: 'No Jest integration case yet.', createSystemUser_post: 'No Jest integration case yet.', updateSystemUser_put: 'No Jest integration case yet.', updateSystemUser_post: 'No Jest integration case yet.', deleteSystemUser: 'No Jest integration case yet.', testPartnerAuth_post: 'No Jest integration case yet.', getPartnerCustomers_get: 'No Jest integration case yet.', getPartnerAircraft_get: 'No Jest integration case yet.', getPartnerCustomerStats_get: 'No Jest integration case yet.', getPartnerJobStats_get: 'No Jest integration case yet.', syncPartnerCustomer_post: 'No Jest integration case yet.', getPartnerServices_get: 'No Jest integration case yet.', }, pilot: { updatePilot_put: 'No Jest integration case yet.', deletePilot: 'Skipped in Jest because the controller requires MongoDB transactions.', }, product: { search_post: 'No Jest integration assertion tied to a distinct behavior yet.', }, user: { createUser_post: 'No Jest integration case yet.', deleteUser: 'No Jest integration case yet.', login_post: 'No Jest integration case yet.', clearTempData_post: 'No Jest integration case yet.', setUserLanguage_post: 'No Jest integration case yet.', getUserDetail_post: 'No Jest integration case yet.', mailPwdReset_post: 'No Jest integration case yet.', validateResetPwdToken_post: 'No Jest integration case yet.', resetPassword_post: 'No Jest integration case yet.', 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.', requestEmailVerification_post: 'No Jest integration case yet.', verifyEmailCode_post: 'No Jest integration case yet.', signup_post: 'No Jest integration case yet.', }, vehicle: { updateVehicle_put: 'No Jest integration case yet.', deleteVehicle: 'Skipped in Jest because the controller requires MongoDB transactions.', unitIdExists_post: 'No Jest integration case yet.', }, }; 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]); } } 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();