api-tests-coverage 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.js +141 -3
- package/dist/src/inference/businessRuleInference.d.ts.map +1 -1
- package/dist/src/inference/businessRuleInference.js +95 -0
- package/dist/src/inference/integrationFlowInference.d.ts +11 -1
- package/dist/src/inference/integrationFlowInference.d.ts.map +1 -1
- package/dist/src/inference/integrationFlowInference.js +49 -2
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -1403,7 +1403,7 @@ program
|
|
|
1403
1403
|
.option('--port <port>', 'Port for the dashboard server (requires --dashboard)', parseInt)
|
|
1404
1404
|
.option('--open', 'Open the dashboard in your browser automatically (requires --dashboard)')
|
|
1405
1405
|
.action(async (options) => {
|
|
1406
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
1406
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
|
|
1407
1407
|
const { metricsPort, serviceName } = setupObservability();
|
|
1408
1408
|
const logger = (0, observability_1.getLogger)();
|
|
1409
1409
|
const configPath = program.opts()['config'];
|
|
@@ -1456,7 +1456,7 @@ program
|
|
|
1456
1456
|
}
|
|
1457
1457
|
// ── 3. Integration flow inference ──────────────────────────────────────
|
|
1458
1458
|
if (doInferFlows) {
|
|
1459
|
-
inferredFlowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings);
|
|
1459
|
+
inferredFlowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings, artifacts.serviceFiles);
|
|
1460
1460
|
const flowsPath = (0, integrationFlowInference_1.writeInferredIntegrationFlows)(inferredFlowsResult, reportsDir);
|
|
1461
1461
|
console.log(`\nIntegration Flow Inference`);
|
|
1462
1462
|
console.log(` Multi-step flows detected in tests: ${inferredFlowsResult.flows.length}`);
|
|
@@ -1514,6 +1514,13 @@ program
|
|
|
1514
1514
|
while ((m = TEST_DECL_RE.exec(content)) !== null) {
|
|
1515
1515
|
descriptions.push(m[2].toLowerCase());
|
|
1516
1516
|
}
|
|
1517
|
+
// Generic: extract test function/method names for Python (def test_xxx),
|
|
1518
|
+
// Ruby (def test_xxx), and similar frameworks where test names are method names.
|
|
1519
|
+
const GENERIC_TEST_FN_RE = /\bdef\s+((?:test|should|spec)_\w+)\s*\(/g;
|
|
1520
|
+
GENERIC_TEST_FN_RE.lastIndex = 0;
|
|
1521
|
+
while ((m = GENERIC_TEST_FN_RE.exec(content)) !== null) {
|
|
1522
|
+
descriptions.push(m[1].replace(/_/g, ' ').toLowerCase());
|
|
1523
|
+
}
|
|
1517
1524
|
}
|
|
1518
1525
|
return [{ file: tf, contentLower, descriptions, isJavaLike }];
|
|
1519
1526
|
});
|
|
@@ -1910,6 +1917,137 @@ program
|
|
|
1910
1917
|
allCoverageResults.push(flowResult);
|
|
1911
1918
|
console.log(` ${syntheticReport.complete}/${syntheticReport.total} multi-step flows detected and covered (100%)`);
|
|
1912
1919
|
}
|
|
1920
|
+
// ── 4f-perf. Performance & resilience coverage ────────────────────────────
|
|
1921
|
+
// When load-test files (JMeter/k6/Gatling) are available, use them.
|
|
1922
|
+
// When none are found, infer performance/resilience evidence from test content.
|
|
1923
|
+
{
|
|
1924
|
+
// ─ Keywords ──────────────────────────────────────────────────────────────
|
|
1925
|
+
const PERF_KEYWORDS = [
|
|
1926
|
+
'load', 'performance', 'stress', 'throughput', 'latency', 'benchmark',
|
|
1927
|
+
'concurrent', 'response time', 'response_time', 'timing', 'timed',
|
|
1928
|
+
'slow', 'fast', 'speed', 'millisecond', 'ms ', ' ms',
|
|
1929
|
+
];
|
|
1930
|
+
const RESILIENCE_KEYWORD_MAP = {
|
|
1931
|
+
'timeout': ['timeout', 'timed out', 'connection timeout', 'request timeout', 'read timeout'],
|
|
1932
|
+
'retry': ['retry', 'retries', 'retried', 'attempt', 'backoff', 'back-off'],
|
|
1933
|
+
'circuit-breaker': ['circuit breaker', 'circuit_breaker', 'circuitbreaker', 'open circuit'],
|
|
1934
|
+
'fallback': ['fallback', 'fall back', 'fall-back', 'default response', 'degraded'],
|
|
1935
|
+
'rate-limiting': ['rate limit', 'rate_limit', 'ratelimit', '429', 'too many requests', 'throttl'],
|
|
1936
|
+
'bulkhead': ['bulkhead', 'semaphore', 'queue full', 'concurrency limit'],
|
|
1937
|
+
};
|
|
1938
|
+
if (artifacts.performanceFiles.length > 0) {
|
|
1939
|
+
// ─ Explicit: JMeter / k6 / Gatling files found ─────────────────────
|
|
1940
|
+
try {
|
|
1941
|
+
console.log(`\nAnalyzing performance coverage (${artifacts.performanceFiles.length} load-test file(s))...`);
|
|
1942
|
+
const endpointItems = (_s = (_r = (_q = allCoverageResults.find((r) => r.type === 'endpoint')) === null || _q === void 0 ? void 0 : _q.details) === null || _r === void 0 ? void 0 : _r.items) !== null && _s !== void 0 ? _s : [];
|
|
1943
|
+
const endpoints = endpointItems.map((item) => {
|
|
1944
|
+
var _a, _b;
|
|
1945
|
+
const parts = item.id.split(' ');
|
|
1946
|
+
return { id: item.id, method: (_a = parts[0]) !== null && _a !== void 0 ? _a : 'GET', path: (_b = parts[1]) !== null && _b !== void 0 ? _b : item.id };
|
|
1947
|
+
});
|
|
1948
|
+
const metricsMap = (0, perfResilienceCoverage_1.parseLoadTestResults)(artifacts.performanceFiles);
|
|
1949
|
+
const perfThresholds = { responseMs: 500, errorRate: 0.05 };
|
|
1950
|
+
const perfCoverages = (0, perfResilienceCoverage_1.analyzePerformanceCoverage)(endpoints, metricsMap, perfThresholds);
|
|
1951
|
+
const scenarios = (0, perfResilienceCoverage_1.buildResilienceScenarios)(endpoints);
|
|
1952
|
+
const resilienceCoverages = await (0, perfResilienceCoverage_1.analyzeResilienceCoverage)(scenarios, testsGlob);
|
|
1953
|
+
const report = (0, perfResilienceCoverage_1.buildPerfResilienceReport)(perfCoverages, resilienceCoverages);
|
|
1954
|
+
const perfResult = {
|
|
1955
|
+
type: 'performance',
|
|
1956
|
+
totalItems: report.totalEndpoints,
|
|
1957
|
+
coveredItems: report.endpointsWithLoadData,
|
|
1958
|
+
coveragePercent: report.performanceCoveragePercent,
|
|
1959
|
+
details: report,
|
|
1960
|
+
};
|
|
1961
|
+
const resilienceResult = {
|
|
1962
|
+
type: 'resilience',
|
|
1963
|
+
totalItems: report.totalResilienceScenarios,
|
|
1964
|
+
coveredItems: report.coveredResilienceScenarios,
|
|
1965
|
+
coveragePercent: report.resilienceCoveragePercent,
|
|
1966
|
+
details: report,
|
|
1967
|
+
};
|
|
1968
|
+
allCoverageResults.push(perfResult, resilienceResult);
|
|
1969
|
+
console.log(` ${report.endpointsWithLoadData}/${report.totalEndpoints} endpoints have load-test data (${report.performanceCoveragePercent}%)`);
|
|
1970
|
+
console.log(` ${report.coveredResilienceScenarios}/${report.totalResilienceScenarios} resilience scenarios covered (${report.resilienceCoveragePercent}%)`);
|
|
1971
|
+
}
|
|
1972
|
+
catch (perfErr) {
|
|
1973
|
+
warnings.push(`Performance coverage failed: ${perfErr instanceof Error ? perfErr.message : String(perfErr)}`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
// ─ Inferred: no load-test files found — scan test content for signals ─
|
|
1978
|
+
console.log(`\nPerformance & resilience coverage (inferred — no JMeter/k6/Gatling files found)...`);
|
|
1979
|
+
// Build a flat list of all known endpoints from earlier coverage results
|
|
1980
|
+
const knownEndpoints = ((_v = (_u = (_t = allCoverageResults.find((r) => r.type === 'endpoint')) === null || _t === void 0 ? void 0 : _t.details) === null || _u === void 0 ? void 0 : _u.items) !== null && _v !== void 0 ? _v : []).map((item) => item.id);
|
|
1981
|
+
const allTestContent = testEntries.map((e) => e.contentLower);
|
|
1982
|
+
const combinedTestContent = allTestContent.join('\n');
|
|
1983
|
+
// ─ Performance inference ────────────────────────────────────────────
|
|
1984
|
+
const hasPerfSignal = PERF_KEYWORDS.some((kw) => combinedTestContent.includes(kw));
|
|
1985
|
+
const perfItems = knownEndpoints.map((endpointId) => {
|
|
1986
|
+
var _a, _b;
|
|
1987
|
+
const parts = endpointId.split(' ');
|
|
1988
|
+
const epPath = ((_a = parts[1]) !== null && _a !== void 0 ? _a : endpointId).toLowerCase();
|
|
1989
|
+
const epLeaf = (_b = epPath.split('/').filter(Boolean).pop()) !== null && _b !== void 0 ? _b : epPath;
|
|
1990
|
+
const hasEvidenceInTests = testEntries.some(({ contentLower }) => {
|
|
1991
|
+
const mentionsEndpoint = epLeaf.length > 2
|
|
1992
|
+
? contentLower.includes(epLeaf)
|
|
1993
|
+
: contentLower.includes(epPath);
|
|
1994
|
+
const hasPerfKw = PERF_KEYWORDS.some((kw) => contentLower.includes(kw));
|
|
1995
|
+
return mentionsEndpoint && hasPerfKw;
|
|
1996
|
+
});
|
|
1997
|
+
return { id: endpointId, hasEvidence: hasEvidenceInTests };
|
|
1998
|
+
});
|
|
1999
|
+
const perfCoveredCount = perfItems.filter((i) => i.hasEvidence).length;
|
|
2000
|
+
const perfTotal = Math.max(perfItems.length, 1); // avoid 0-denominator
|
|
2001
|
+
const perfPct = knownEndpoints.length === 0
|
|
2002
|
+
? (hasPerfSignal ? 30 : 0) // no routes but some signal
|
|
2003
|
+
: Math.round((perfCoveredCount / perfItems.length) * 100);
|
|
2004
|
+
const perfResult = {
|
|
2005
|
+
type: 'performance',
|
|
2006
|
+
totalItems: perfItems.length || 1,
|
|
2007
|
+
coveredItems: knownEndpoints.length === 0 && hasPerfSignal ? 0 : perfCoveredCount,
|
|
2008
|
+
coveragePercent: perfPct,
|
|
2009
|
+
details: {
|
|
2010
|
+
inferred: true,
|
|
2011
|
+
note: 'No JMeter, k6, or Gatling files found. Coverage inferred from test content keywords.',
|
|
2012
|
+
totalEndpoints: perfItems.length,
|
|
2013
|
+
endpointsWithEvidence: perfCoveredCount,
|
|
2014
|
+
performanceCoveragePercent: perfPct,
|
|
2015
|
+
items: perfItems,
|
|
2016
|
+
},
|
|
2017
|
+
};
|
|
2018
|
+
allCoverageResults.push(perfResult);
|
|
2019
|
+
if (knownEndpoints.length > 0) {
|
|
2020
|
+
console.log(` ${perfCoveredCount}/${perfItems.length} endpoints have performance test evidence (${perfPct}%)`);
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
console.log(` Performance signal in tests: ${hasPerfSignal ? 'yes' : 'none'} (0% — no load-test files)`);
|
|
2024
|
+
}
|
|
2025
|
+
console.log(` [NOTE] Add JMeter .jtl/.csv, k6 .json, or Gatling simulation.log files for accurate load-test metrics.`);
|
|
2026
|
+
// ─ Resilience inference ──────────────────────────────────────────────
|
|
2027
|
+
const resilienceItems = Object.entries(RESILIENCE_KEYWORD_MAP).map(([category, keywords]) => {
|
|
2028
|
+
const covered = testEntries.some(({ contentLower }) => keywords.some((kw) => contentLower.includes(kw)));
|
|
2029
|
+
return { id: `resilience:${category}`, category, covered };
|
|
2030
|
+
});
|
|
2031
|
+
const resCoveredCount = resilienceItems.filter((i) => i.covered).length;
|
|
2032
|
+
const resPct = Math.round((resCoveredCount / resilienceItems.length) * 100);
|
|
2033
|
+
const resilienceResult = {
|
|
2034
|
+
type: 'resilience',
|
|
2035
|
+
totalItems: resilienceItems.length,
|
|
2036
|
+
coveredItems: resCoveredCount,
|
|
2037
|
+
coveragePercent: resPct,
|
|
2038
|
+
details: {
|
|
2039
|
+
inferred: true,
|
|
2040
|
+
note: 'Resilience coverage inferred from test content. No load-test files found.',
|
|
2041
|
+
totalResilienceScenarios: resilienceItems.length,
|
|
2042
|
+
coveredResilienceScenarios: resCoveredCount,
|
|
2043
|
+
resilienceCoveragePercent: resPct,
|
|
2044
|
+
items: resilienceItems,
|
|
2045
|
+
},
|
|
2046
|
+
};
|
|
2047
|
+
allCoverageResults.push(resilienceResult);
|
|
2048
|
+
console.log(` ${resCoveredCount}/${resilienceItems.length} resilience categories have test evidence (${resPct}%)`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
1913
2051
|
if (allCoverageResults.length > 0) {
|
|
1914
2052
|
const observabilityInfo = (0, observability_1.buildObservabilityInfo)(metricsPort);
|
|
1915
2053
|
(0, reporting_1.generateMultiFormatReports)(allCoverageResults, ['json'], reportsDir, {}, observabilityInfo);
|
|
@@ -2019,7 +2157,7 @@ program
|
|
|
2019
2157
|
if (options['dashboard']) {
|
|
2020
2158
|
(0, serveDashboard_1.serveDashboard)({
|
|
2021
2159
|
reportsDir: reportsDir,
|
|
2022
|
-
port: (
|
|
2160
|
+
port: (_w = options['port']) !== null && _w !== void 0 ? _w : 4000,
|
|
2023
2161
|
open: Boolean(options['open']),
|
|
2024
2162
|
});
|
|
2025
2163
|
// Keep the process alive — the HTTP server holds the event loop open
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"businessRuleInference.d.ts","sourceRoot":"","sources":["../../../src/inference/businessRuleInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AACjD,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,YAAY,CAAC;AAEjB,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"businessRuleInference.d.ts","sourceRoot":"","sources":["../../../src/inference/businessRuleInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AACjD,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,YAAY,CAAC;AAEjB,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAwPD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA8C3E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EAAE,EACtB,QAAQ,GAAE,MAAM,EAAO,GACtB,2BAA2B,CAuB7B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,2BAA2B,EACnC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR;AAID,eAAO,MAAM,kBAAkB,aAM7B,CAAC;AAEH;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwCnE"}
|
|
@@ -138,12 +138,104 @@ const INFERENCE_PATTERNS = [
|
|
|
138
138
|
behaviorTemplate: (m) => `${m[1]} must not exceed ${m[2]}`,
|
|
139
139
|
conditionTemplate: (m) => `${m[1]} > ${m[2]}`,
|
|
140
140
|
},
|
|
141
|
+
// ── Flask / Python-specific patterns ──────────────────────────────────────
|
|
142
|
+
// Broader Python raise: any raise X() or raise X.y() not already ending in Exception/Error
|
|
143
|
+
// (Ordered after the existing raise patterns so deduplification by source location handles overlap)
|
|
144
|
+
{
|
|
145
|
+
type: 'business_logic',
|
|
146
|
+
pattern: /\braise\s+([\w]+(?:\.[\w]+)*)\s*(?:\(([^)]{0,60})\))?/,
|
|
147
|
+
nameTemplate: (m) => { var _a; return toSnakeCase((_a = m[1].split('.').pop()) !== null && _a !== void 0 ? _a : m[1]); },
|
|
148
|
+
behaviorTemplate: (m) => `Operation rejected: ${m[1]}`,
|
|
149
|
+
conditionTemplate: (m) => { var _a; return `raise ${m[1]}(${((_a = m[2]) !== null && _a !== void 0 ? _a : '').trim()})`; },
|
|
150
|
+
},
|
|
151
|
+
// Python `if not X:` null guard — idiomatic Python nil check
|
|
152
|
+
{
|
|
153
|
+
type: 'validation',
|
|
154
|
+
pattern: /if\s+not\s+([\w.]+)\s*(?::|and|or)/,
|
|
155
|
+
nameTemplate: (m) => `require_${toSnakeCase(m[1].replace(/\./g, '_'))}`,
|
|
156
|
+
behaviorTemplate: (m) => `${m[1]} must exist`,
|
|
157
|
+
conditionTemplate: (m) => `if not ${m[1]}`,
|
|
158
|
+
},
|
|
159
|
+
// Flask/Django auth decorators: @jwt_required, @login_required, etc.
|
|
160
|
+
{
|
|
161
|
+
type: 'authorization',
|
|
162
|
+
pattern: /@(jwt_required|login_required|require_auth|requires_auth|permission_required|auth_required|authenticated_user|require_permissions?)\b/i,
|
|
163
|
+
nameTemplate: (m) => `require_${toSnakeCase(m[1])}`,
|
|
164
|
+
behaviorTemplate: (m) => `Request requires authentication (${m[1]})`,
|
|
165
|
+
conditionTemplate: (m) => `@${m[1]}`,
|
|
166
|
+
},
|
|
167
|
+
// Flask `@use_kwargs` / `@validate_arguments` input validation
|
|
168
|
+
{
|
|
169
|
+
type: 'validation',
|
|
170
|
+
pattern: /@(use_kwargs|validate_arguments?|expects_json|validate_body)\s*\(\s*([\w]+)/i,
|
|
171
|
+
nameTemplate: (m) => { var _a; return `validate_input_${toSnakeCase((_a = m[2]) !== null && _a !== void 0 ? _a : 'schema')}`; },
|
|
172
|
+
behaviorTemplate: (m) => { var _a; return `Input validated against ${(_a = m[2]) !== null && _a !== void 0 ? _a : 'schema'}`; },
|
|
173
|
+
conditionTemplate: (m) => { var _a; return `@${m[1]}(${(_a = m[2]) !== null && _a !== void 0 ? _a : ''})`; },
|
|
174
|
+
},
|
|
175
|
+
// Python ownership / attribute comparison guard
|
|
176
|
+
{
|
|
177
|
+
type: 'authorization',
|
|
178
|
+
pattern: /if\s+[\w.]+\s*!=\s*[\w.]+\.(?:id|user_id|author_id|owner_id|profile\.id)/,
|
|
179
|
+
nameTemplate: () => 'ownership_check',
|
|
180
|
+
behaviorTemplate: () => 'Resource must belong to the requesting user',
|
|
181
|
+
conditionTemplate: (m) => m[0].trim(),
|
|
182
|
+
},
|
|
183
|
+
// ── Generic JS/TS/frontend patterns ───────────────────────────────────────
|
|
184
|
+
// Generic JS/TS `if (!expression)` null/falsy guard
|
|
185
|
+
// Matches meaningful presence checks like !JWT.get(), !User.current, !token
|
|
186
|
+
// Skips pure normalisation calls like .trim(), .length, .toLowerCase(), etc.
|
|
187
|
+
// via the skipIfGroup1Matches post-match filter.
|
|
188
|
+
{
|
|
189
|
+
type: 'validation',
|
|
190
|
+
pattern: /if\s*\(\s*!\s*([\w][\w.[\]]*(?:\.\w+\(\s*\))?)\s*\)/,
|
|
191
|
+
skipIfGroup1Matches: /\.(trim|length|split|join|toLowerCase|toUpperCase|toString|valueOf|slice|substr|substring|replace|indexOf|includes|startsWith|endsWith)\(\s*\)$/,
|
|
192
|
+
nameTemplate: (m) => `require_${toSnakeCase(m[1].replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, ''))}`,
|
|
193
|
+
behaviorTemplate: (m) => `${m[1]} must be present/truthy`,
|
|
194
|
+
conditionTemplate: (m) => `!${m[1]}`,
|
|
195
|
+
},
|
|
196
|
+
// HTTP status comparison guard (frontend interceptors): rejection.status === 401
|
|
197
|
+
{
|
|
198
|
+
type: 'validation',
|
|
199
|
+
pattern: /\.status\s*={1,3}\s*(4\d\d|5\d\d)/,
|
|
200
|
+
nameTemplate: (m) => `http_status_${m[1]}_check`,
|
|
201
|
+
behaviorTemplate: (m) => `Handle HTTP ${m[1]} error response`,
|
|
202
|
+
conditionTemplate: (m) => `status == ${m[1]}`,
|
|
203
|
+
},
|
|
204
|
+
// Ownership / identity comparison guard (username === author.username)
|
|
205
|
+
{
|
|
206
|
+
type: 'authorization',
|
|
207
|
+
pattern: /if\s*\(?\s*[\w.]+\.(?:username|userId|user_id|email|id)\s*!==?\s*[\w.]+\.(?:username|userId|user_id|email|id|author\.username)/,
|
|
208
|
+
nameTemplate: () => 'ownership_identity_check',
|
|
209
|
+
behaviorTemplate: () => 'Resource belongs to a specific user identity',
|
|
210
|
+
conditionTemplate: (m) => m[0].trim().replace(/^if\s*\(?/, '').replace(/\)?\s*$/, ''),
|
|
211
|
+
},
|
|
212
|
+
// Generic early-return guard: if (cond) return/throw on same or adjacent line
|
|
213
|
+
{
|
|
214
|
+
type: 'business_logic',
|
|
215
|
+
pattern: /if\s*\([^)]{3,80}\)\s*(?:\{[^}]{0,40}\})?\s*(?:return|throw|raise)\b/,
|
|
216
|
+
nameTemplate: () => 'guard_condition',
|
|
217
|
+
behaviorTemplate: () => 'Early return or error on condition',
|
|
218
|
+
conditionTemplate: (m) => {
|
|
219
|
+
const cond = m[0].match(/if\s*\(([^)]{3,80})\)/);
|
|
220
|
+
return cond ? cond[1].trim() : m[0].trim();
|
|
221
|
+
},
|
|
222
|
+
},
|
|
141
223
|
];
|
|
142
224
|
// ─── Endpoint heuristics ──────────────────────────────────────────────────────
|
|
143
225
|
/** Attempt to associate a rule with the nearest HTTP route/endpoint annotation. */
|
|
144
226
|
function guessEndpoint(lines, ruleLineIdx) {
|
|
145
227
|
// Search up to 40 lines above for common routing patterns
|
|
146
228
|
const lookupLines = lines.slice(Math.max(0, ruleLineIdx - 40), ruleLineIdx);
|
|
229
|
+
// Flask: @blueprint.route('/path', methods=...) / @app.route('/path', ...)
|
|
230
|
+
for (let i = lookupLines.length - 1; i >= 0; i--) {
|
|
231
|
+
const fm = lookupLines[i].match(/@\w+\.route\s*\(\s*['"]([^'"]+)['"]/);
|
|
232
|
+
if (fm) {
|
|
233
|
+
// Try to find method from same line or near
|
|
234
|
+
const methodsMatch = lookupLines[i].match(/methods\s*=\s*[\[(]['"]?([\w]+)['"]?/);
|
|
235
|
+
const method = methodsMatch ? methodsMatch[1].toUpperCase() : 'GET';
|
|
236
|
+
return `${method} ${fm[1]}`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
147
239
|
// Spring: @GetMapping("/path") / @PostMapping / @RequestMapping
|
|
148
240
|
for (let i = lookupLines.length - 1; i >= 0; i--) {
|
|
149
241
|
const m = lookupLines[i].match(/@(Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*["']([^"']+)["']/i);
|
|
@@ -191,6 +283,9 @@ function inferRulesFromFile(filePath) {
|
|
|
191
283
|
const match = line.match(ip.pattern);
|
|
192
284
|
if (!match)
|
|
193
285
|
continue;
|
|
286
|
+
// Post-match filter: skip false positives based on first captured group
|
|
287
|
+
if (ip.skipIfGroup1Matches && match[1] && ip.skipIfGroup1Matches.test(match[1]))
|
|
288
|
+
continue;
|
|
194
289
|
const condition = ip.conditionTemplate(match);
|
|
195
290
|
const sourceLocation = `${filePath}:${lineIdx + 1}`;
|
|
196
291
|
const dedupKey = `${filePath}:${lineIdx}:${condition}`;
|
|
@@ -44,10 +44,20 @@ export interface IntegrationFlowInferenceResult {
|
|
|
44
44
|
* Infer integration flows from a single test file.
|
|
45
45
|
*/
|
|
46
46
|
export declare function inferFlowsFromFile(filePath: string): InferredIntegrationFlow[];
|
|
47
|
+
/**
|
|
48
|
+
* Infer integration flows from service source files (not test files).
|
|
49
|
+
*
|
|
50
|
+
* Used as a fallback when no test files are present (e.g. frontend-only projects).
|
|
51
|
+
* Only produces flows for source files that contain 2+ HTTP calls in the same function.
|
|
52
|
+
*/
|
|
53
|
+
export declare function inferFlowsFromSourceFiles(serviceFiles: string[]): InferredIntegrationFlow[];
|
|
47
54
|
/**
|
|
48
55
|
* Run flow inference across all provided test files.
|
|
56
|
+
*
|
|
57
|
+
* When `testFiles` is empty and `serviceFiles` is provided, flows are inferred
|
|
58
|
+
* from the service source code instead and tagged accordingly.
|
|
49
59
|
*/
|
|
50
|
-
export declare function inferIntegrationFlows(testFiles: string[], warnings?: string[]): IntegrationFlowInferenceResult;
|
|
60
|
+
export declare function inferIntegrationFlows(testFiles: string[], warnings?: string[], serviceFiles?: string[]): IntegrationFlowInferenceResult;
|
|
51
61
|
/**
|
|
52
62
|
* Write inferred integration flows to the reports directory.
|
|
53
63
|
* Returns the path of the written file.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrationFlowInference.d.ts","sourceRoot":"","sources":["../../../src/inference/integrationFlowInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,+BAA+B;IAC/B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"integrationFlowInference.d.ts","sourceRoot":"","sources":["../../../src/inference/integrationFlowInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,+BAA+B;IAC/B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAqLD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAW9E;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,uBAAuB,EAAE,CAgB3F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EAAE,EACnB,QAAQ,GAAE,MAAM,EAAO,EACvB,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,8BAA8B,CAoChC;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,8BAA8B,EACtC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR"}
|
|
@@ -48,6 +48,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
48
|
})();
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
50
|
exports.inferFlowsFromFile = inferFlowsFromFile;
|
|
51
|
+
exports.inferFlowsFromSourceFiles = inferFlowsFromSourceFiles;
|
|
51
52
|
exports.inferIntegrationFlows = inferIntegrationFlows;
|
|
52
53
|
exports.writeInferredIntegrationFlows = writeInferredIntegrationFlows;
|
|
53
54
|
const fs = __importStar(require("fs"));
|
|
@@ -63,6 +64,11 @@ const HTTP_CALL_PATTERNS = [
|
|
|
63
64
|
{ pattern: /\b(get|post|put|patch|delete)\s+['"`]([^'"`\s]+)['"`]/i, methodGroup: 1, pathGroup: 2 },
|
|
64
65
|
// fetch('/path', { method: 'POST' })
|
|
65
66
|
{ pattern: /fetch\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*\{[^}]*method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE|HEAD)['"`]/i, methodGroup: 2, pathGroup: 1 },
|
|
67
|
+
// WebTest TestApp: testapp.post_json(url_for('endpoint'), data)
|
|
68
|
+
// Also handles: testapp.get(url_for('endpoint')), testapp.delete_json(...)
|
|
69
|
+
{ pattern: /(?:testapp|self\.testapp)\.(get_json|post_json|put_json|patch_json|delete_json|get|post|put|patch|delete)\s*\(\s*url_for\s*\(\s*['"]([^'"]+)['"]/i, methodGroup: 1, pathGroup: 2 },
|
|
70
|
+
// WebTest without url_for: testapp.get('/path')
|
|
71
|
+
{ pattern: /(?:testapp|self\.testapp)\.(get_json|post_json|put_json|patch_json|delete_json|get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/i, methodGroup: 1, pathGroup: 2 },
|
|
66
72
|
];
|
|
67
73
|
/** Patterns for test function / scenario boundaries */
|
|
68
74
|
const TEST_FUNCTION_PATTERNS = [
|
|
@@ -85,6 +91,8 @@ function extractHttpCallsFromLines(lines) {
|
|
|
85
91
|
const m = line.match(p.pattern);
|
|
86
92
|
if (m) {
|
|
87
93
|
const method = m[p.methodGroup].toUpperCase();
|
|
94
|
+
// Normalize WebTest _json suffix: POST_JSON → POST, GET_JSON → GET
|
|
95
|
+
const normalizedMethod = method.replace(/_JSON$/, '');
|
|
88
96
|
const rawPath = m[p.pathGroup];
|
|
89
97
|
// Skip unlikely paths (full URLs with domain, non-path values)
|
|
90
98
|
if (rawPath.startsWith('http') && !rawPath.includes('/api'))
|
|
@@ -92,7 +100,7 @@ function extractHttpCallsFromLines(lines) {
|
|
|
92
100
|
const cleanPath = extractPathFromUrl(rawPath);
|
|
93
101
|
if (!cleanPath)
|
|
94
102
|
continue;
|
|
95
|
-
calls.push({ method, path: cleanPath, lineIdx: i });
|
|
103
|
+
calls.push({ method: normalizedMethod, path: cleanPath, lineIdx: i });
|
|
96
104
|
break; // only first pattern match per line
|
|
97
105
|
}
|
|
98
106
|
}
|
|
@@ -102,6 +110,9 @@ function extractHttpCallsFromLines(lines) {
|
|
|
102
110
|
function extractPathFromUrl(raw) {
|
|
103
111
|
if (raw.startsWith('/'))
|
|
104
112
|
return raw;
|
|
113
|
+
// url_for endpoint name: 'blueprint.function' — treat as pseudo-path
|
|
114
|
+
if (/^\w+\.\w+$/.test(raw))
|
|
115
|
+
return `/${raw.replace('.', '/')}`;
|
|
105
116
|
try {
|
|
106
117
|
const u = new URL(raw);
|
|
107
118
|
return u.pathname || undefined;
|
|
@@ -203,10 +214,46 @@ function inferFlowsFromFile(filePath) {
|
|
|
203
214
|
const calls = extractHttpCallsFromLines(lines);
|
|
204
215
|
return groupCallsIntoFlows(calls, filePath, lines);
|
|
205
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Infer integration flows from service source files (not test files).
|
|
219
|
+
*
|
|
220
|
+
* Used as a fallback when no test files are present (e.g. frontend-only projects).
|
|
221
|
+
* Only produces flows for source files that contain 2+ HTTP calls in the same function.
|
|
222
|
+
*/
|
|
223
|
+
function inferFlowsFromSourceFiles(serviceFiles) {
|
|
224
|
+
const allFlows = [];
|
|
225
|
+
for (const fp of serviceFiles) {
|
|
226
|
+
let content;
|
|
227
|
+
try {
|
|
228
|
+
content = fs.readFileSync(fp, 'utf-8');
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const lines = content.split('\n');
|
|
234
|
+
const calls = extractHttpCallsFromLines(lines);
|
|
235
|
+
if (calls.length < 2)
|
|
236
|
+
continue;
|
|
237
|
+
const flows = groupCallsIntoFlows(calls, fp, lines);
|
|
238
|
+
allFlows.push(...flows);
|
|
239
|
+
}
|
|
240
|
+
return allFlows;
|
|
241
|
+
}
|
|
206
242
|
/**
|
|
207
243
|
* Run flow inference across all provided test files.
|
|
244
|
+
*
|
|
245
|
+
* When `testFiles` is empty and `serviceFiles` is provided, flows are inferred
|
|
246
|
+
* from the service source code instead and tagged accordingly.
|
|
208
247
|
*/
|
|
209
|
-
function inferIntegrationFlows(testFiles, warnings = []) {
|
|
248
|
+
function inferIntegrationFlows(testFiles, warnings = [], serviceFiles) {
|
|
249
|
+
if (testFiles.length === 0 && serviceFiles && serviceFiles.length > 0) {
|
|
250
|
+
warnings.push('No test files found; integration flows inferred from service source code.');
|
|
251
|
+
const flows = inferFlowsFromSourceFiles(serviceFiles);
|
|
252
|
+
if (flows.length === 0) {
|
|
253
|
+
warnings.push('No multi-step HTTP call sequences detected in service source files; integration flow inference produced no results.');
|
|
254
|
+
}
|
|
255
|
+
return { flows, filesAnalyzed: serviceFiles.length, inferred: true, warnings };
|
|
256
|
+
}
|
|
210
257
|
if (testFiles.length === 0) {
|
|
211
258
|
warnings.push('No test files provided; integration flow inference skipped.');
|
|
212
259
|
return { flows: [], filesAnalyzed: 0, inferred: true, warnings };
|
package/package.json
CHANGED