274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
'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(); |