86 lines
5.0 KiB
JavaScript
86 lines
5.0 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { classify, parseEffort, groupIssues, hasMarker, issueBody, fetchIssues, main, loadConfig } from '../scripts/sonar_triage.mjs';
|
|
|
|
const cfg = { allow: new Set(['python:S1481']), skip: [{ rule: 'javascript:S2245', file: '^static/js/', reason: 'ids' }] };
|
|
const mk = (o) => ({ key: 'K' + Math.random(), rule: 'python:S1481', severity: 'MINOR', type: 'CODE_SMELL', effort: '2min', component: 'p:core/x.py', line: 3, message: 'm', ...o });
|
|
|
|
test('effort parsing', () => { assert.equal(parseEffort('1h30min'), 90); assert.equal(parseEffort('5min'), 5); assert.equal(parseEffort(undefined), 0); });
|
|
test('small: allowlisted + minor in core', () => assert.equal(classify(mk(), cfg).cls, 'small'));
|
|
test('small: MAJOR only with effort <= 5min', () => {
|
|
assert.equal(classify(mk({ severity: 'MAJOR' }), cfg).cls, 'small');
|
|
assert.equal(classify(mk({ severity: 'MAJOR', effort: '10min' }), cfg).cls, 'medium');
|
|
});
|
|
test('not allowlisted -> medium; critical or >60min -> large', () => {
|
|
assert.equal(classify(mk({ rule: 'python:S9' }), cfg).cls, 'medium');
|
|
assert.equal(classify(mk({ rule: 'python:S9', severity: 'CRITICAL' }), cfg).cls, 'large');
|
|
assert.equal(classify(mk({ rule: 'python:S9', effort: '2h' }), cfg).cls, 'large');
|
|
});
|
|
test('never small: vulnerability, outside dirs, risky path/message', () => {
|
|
assert.notEqual(classify(mk({ type: 'VULNERABILITY' }), cfg).cls, 'small');
|
|
assert.notEqual(classify(mk({ component: 'p:tests/a.py' }), cfg).cls, 'small');
|
|
assert.notEqual(classify(mk({ component: 'p:core/auth.py' }), cfg).cls, 'small');
|
|
assert.notEqual(classify(mk({ message: 'unused var in await call' }), cfg).cls, 'small');
|
|
});
|
|
test('skip list wins', () => assert.equal(classify(mk({ rule: 'javascript:S2245', component: 'p:static/js/a.js' }), cfg).cls, 'skipped'));
|
|
test('shipped config loads and skips server.py bind', () => {
|
|
const c = loadConfig();
|
|
assert.equal(classify(mk({ rule: 'python:S104', component: 'p:server.py' }), c).cls, 'skipped');
|
|
});
|
|
const P = (i) => ({ ...i, path: i.component.slice(2) });
|
|
test('grouping per rule across files, marker keeps dedupe', () => {
|
|
const g = groupIssues([mk({ key: 'b' }), mk({ key: 'a' }), mk({ key: 'c', component: 'p:core/y.py' }), mk({ key: 'd', rule: 'python:S9' })].map(P));
|
|
assert.equal(g.length, 2);
|
|
const one = g.find((x) => x.rule === 'python:S1481');
|
|
assert.equal(one.files, 2);
|
|
const body = issueBody(one);
|
|
assert.match(body, /<!-- sonar-keys: a,b,c -->/);
|
|
assert.match(body, /\| `core\/y.py` \| 3 \| c \|/);
|
|
assert.ok(hasMarker([body], ['b']));
|
|
assert.ok(!hasMarker([body], ['zzz']));
|
|
assert.doesNotMatch(one.title, /part/);
|
|
});
|
|
test('rule with >40 locations is split into numbered parts', () => {
|
|
const many = Array.from({ length: 85 }, (_, n) => P(mk({ key: 'k' + String(n).padStart(3, '0'), component: `p:core/f${n % 7}.py`, line: n })));
|
|
const g = groupIssues(many);
|
|
assert.deepEqual(g.map((x) => x.issues.length), [40, 40, 5]);
|
|
assert.match(g[0].title, /part 1\/3/); assert.match(g[2].title, /part 3\/3/);
|
|
assert.equal(new Set(g.flatMap((x) => x.keys)).size, 85);
|
|
assert.doesNotMatch(issueBody(g[0]), /and \d+ more/);
|
|
});
|
|
test('body caps at 40 rows with "and N more" but marker lists all keys', () => {
|
|
const g = groupIssues(Array.from({ length: 45 }, (_, n) => P(mk({ key: 'k' + String(n).padStart(2, '0'), line: n }))), 100)[0];
|
|
const body = issueBody(g);
|
|
assert.equal(body.split('\n').filter((l) => l.startsWith('| `core/x.py`')).length, 40);
|
|
assert.match(body, /and 5 more/);
|
|
assert.ok(hasMarker([body], ['k44']));
|
|
});
|
|
test('per-run cap limits created issues (dry-run)', async () => {
|
|
const out = [];
|
|
const issues = Array.from({ length: 12 }, (_, n) => mk({ key: 'z' + n, rule: 'python:R' + n }));
|
|
await main(['--dry-run', '--all'], { token: 't', cfg, log: (s) => out.push(s), fetchFn: async () => page(issues) });
|
|
const txt = out.join('\n');
|
|
assert.match(txt, /Medium\+ groups \(per rule, max 40 locations\): 12/);
|
|
assert.match(txt, /10 of \d+ to create \(cap 10\)|gh unauthenticated/);
|
|
});
|
|
|
|
const page = (issues) => ({ ok: true, json: async () => ({ issues, paging: { total: issues.length } }) });
|
|
|
|
test('fetch sends token as basic username, new-code filter', async () => {
|
|
let seen;
|
|
await fetchIssues({ token: 'SECRET', fetchFn: async (u, o) => { seen = { u, o }; return page([]); } });
|
|
assert.equal(seen.o.headers.Authorization, 'Basic ' + Buffer.from('SECRET:').toString('base64'));
|
|
assert.match(seen.u, /inNewCodePeriod=true/);
|
|
assert.doesNotMatch(seen.u, /SECRET/);
|
|
});
|
|
test('dry-run makes only GET requests and never prints the token', async () => {
|
|
const methods = []; const out = [];
|
|
await main(['--dry-run', '--all'], {
|
|
token: 'SECRET-TOKEN-123', cfg, noActions: true, log: (s) => out.push(s),
|
|
fetchFn: async (u, o) => { methods.push(o.method || 'GET'); return page([mk(), mk({ rule: 'python:S9' })]); },
|
|
});
|
|
assert.deepEqual([...new Set(methods)], ['GET']);
|
|
assert.ok(out.join('\n').includes('SMALL (1)'));
|
|
assert.ok(!out.join('\n').includes('SECRET-TOKEN-123'));
|
|
});
|