163 lines
6.0 KiB
JavaScript
163 lines
6.0 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 = {
|
|
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(); |