110 lines
6.7 KiB
JavaScript
110 lines
6.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { contractViolations } from './analyze-firstleg-dependencies.mjs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
const moduleRoot = path.resolve(import.meta.dirname, '..');
|
|
const analyzer = path.join(moduleRoot, 'scripts', 'analyze-firstleg-dependencies.mjs');
|
|
const contract = path.join(moduleRoot, 'src', 'test', 'resources', 'architecture', 'firstleg-slice-dependencies.json');
|
|
|
|
test('reviewed original-domain calls do not open an entire slice or allow wildcard grants', () => {
|
|
const report=analyze(); const baseline=JSON.parse(readFileSync(contract,'utf8'));
|
|
assert.deepEqual(contractViolations(report,baseline),[]);
|
|
report.classDependencies.push({source:'com.baiyue.firstleg.service.SmartLoadingService',target:'com.baiyue.firstleg.domain.PriceBookChannelCatalog'});
|
|
assert.ok(contractViolations(report,baseline).some(message=>message.includes('SmartLoadingService->com.baiyue.firstleg.domain.PriceBookChannelCatalog')));
|
|
baseline.reviewedClassDependencies.push({source:'com.baiyue.firstleg.*',target:'com.baiyue.firstleg.service.FbaPricingService',reason:'invalid wildcard'});
|
|
assert.ok(contractViolations(report,baseline).some(message=>message.includes('exact classes')));
|
|
report.packageSccs.push(['com.baiyue.firstleg.service','com.baiyue.firstleg.pricebookquery.application']);
|
|
assert.ok(contractViolations(report,baseline).some(message=>message.includes('unreviewed package cycle')));
|
|
});
|
|
|
|
function analyze() {
|
|
return JSON.parse(execFileSync(process.execPath, [analyzer, '--source-set', 'main', '--format', 'json'], {
|
|
cwd: moduleRoot,
|
|
encoding: 'utf8',
|
|
}));
|
|
}
|
|
|
|
test('current source does not add slice dependencies beyond the baseline', () => {
|
|
execFileSync(process.execPath, [analyzer, '--source-set', 'main', '--format', 'dot', '--check', contract], {
|
|
cwd: moduleRoot,
|
|
encoding: 'utf8',
|
|
});
|
|
});
|
|
|
|
test('customer statistics remains an independently packaged low-coupling slice', () => {
|
|
const report = analyze();
|
|
const classes = report.classes.filter(entry => entry.slice === 'customer-statistics');
|
|
assert.deepEqual(classes.map(entry => entry.fqcn).sort(), [
|
|
'com.baiyue.firstleg.customerstatistics.adapter.in.web.PlatformCustomerTransitStatisticsController',
|
|
'com.baiyue.firstleg.customerstatistics.adapter.out.persistence.CustomerTransitStatisticsMapper',
|
|
'com.baiyue.firstleg.customerstatistics.application.CustomerTransitStatisticsService',
|
|
'com.baiyue.firstleg.customerstatistics.application.model.CustomerTransitStatisticsDtos',
|
|
'com.baiyue.firstleg.customerstatistics.application.port.out.CustomerTransitStatisticsReader',
|
|
]);
|
|
const outgoing = report.sliceEdges.filter(edge => edge.source === 'customer-statistics');
|
|
const incoming = report.sliceEdges.filter(edge => edge.target === 'customer-statistics');
|
|
assert.deepEqual(outgoing, [{ source: 'customer-statistics', target: 'shared-technical', count: 1 }]);
|
|
assert.deepEqual(incoming, []);
|
|
assert.equal(report.sliceSccs.some(component => component.includes('customer-statistics')), false);
|
|
});
|
|
|
|
test('customer statistics application layer only depends inward', () => {
|
|
const report = analyze();
|
|
const prefix = 'com.baiyue.firstleg.customerstatistics.application';
|
|
const applicationClasses = report.classes.filter(entry => entry.packageName.startsWith(prefix));
|
|
assert.equal(applicationClasses.length, 3);
|
|
for (const entry of applicationClasses) {
|
|
assert.equal(entry.imports.some(imported => imported.startsWith('org.springframework.web')), false);
|
|
assert.equal(entry.imports.some(imported => imported.startsWith('org.apache.ibatis')), false);
|
|
assert.equal(entry.imports.some(imported => imported.startsWith('com.baomidou.mybatisplus')), false);
|
|
}
|
|
const outwardDependencies = report.classDependencies.filter(edge => edge.source.startsWith(prefix)
|
|
&& edge.target.startsWith('com.baiyue.firstleg.') && !edge.target.startsWith(prefix));
|
|
assert.deepEqual(outwardDependencies, []);
|
|
});
|
|
|
|
test('price book query remains a leaf slice outside reviewed pre-existing package cycles', () => {
|
|
const report = analyze();
|
|
const classes = report.classes.filter(entry => entry.slice === 'pricebook-query');
|
|
assert.deepEqual(classes.map(entry => entry.fqcn).sort(), [
|
|
'com.baiyue.firstleg.pricebookquery.adapter.out.persistence.PriceBookQueryMapper',
|
|
'com.baiyue.firstleg.pricebookquery.application.PriceBookQueryService',
|
|
'com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows',
|
|
'com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase',
|
|
'com.baiyue.firstleg.pricebookquery.application.port.out.PriceBookQueryReader',
|
|
]);
|
|
assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'pricebook-query'), []);
|
|
assert.deepEqual(report.sliceEdges.filter(edge => edge.target === 'pricebook-query')
|
|
.map(edge => edge.source).sort(), ['booking-customs-trucking', 'measurement-pricing', 'split-loading']);
|
|
assert.equal(report.sliceSccs.some(component => component.includes('pricebook-query')), false);
|
|
assert.equal(report.packageSccs.some(cycle=>cycle.some(pkg=>pkg.startsWith('com.baiyue.firstleg.pricebookquery'))),false);
|
|
const reviewed=JSON.parse(readFileSync(contract,'utf8')).reviewedPackageCycles.map(c=>c.packages);
|
|
assert.deepEqual(report.packageSccs,reviewed);
|
|
});
|
|
|
|
test('price book query application and consumers respect ports', () => {
|
|
const report = analyze();
|
|
const prefix = 'com.baiyue.firstleg.pricebookquery.application';
|
|
const applicationClasses = report.classes.filter(entry => entry.packageName.startsWith(prefix));
|
|
assert.equal(applicationClasses.length, 4);
|
|
for (const entry of applicationClasses) {
|
|
assert.equal(entry.imports.some(imported => imported.startsWith('org.springframework.web')), false);
|
|
assert.equal(entry.imports.some(imported => imported.startsWith('org.apache.ibatis')), false);
|
|
assert.equal(entry.imports.some(imported => imported.startsWith('com.baomidou.mybatisplus')), false);
|
|
}
|
|
const outwardDependencies = report.classDependencies.filter(edge => edge.source.startsWith(prefix)
|
|
&& edge.target.startsWith('com.baiyue.firstleg.') && !edge.target.startsWith(prefix));
|
|
assert.deepEqual(outwardDependencies, []);
|
|
const allowedTargets = [
|
|
'com.baiyue.firstleg.pricebookquery.application.model.',
|
|
'com.baiyue.firstleg.pricebookquery.application.port.in.',
|
|
];
|
|
const implementationLeaks = report.classDependencies.filter(edge => !edge.source.startsWith('com.baiyue.firstleg.pricebookquery.')
|
|
&& edge.target.startsWith('com.baiyue.firstleg.pricebookquery.')
|
|
&& !allowedTargets.some(prefixValue => edge.target.startsWith(prefixValue)));
|
|
assert.deepEqual(implementationLeaks, []);
|
|
});
|