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'); }); test('grouping by rule+file and body marker', () => { const g = groupIssues([mk({ key: 'b' }), mk({ key: 'a' }), mk({ key: 'c', component: 'p:core/y.py' })].map((i) => ({ ...i, path: i.component.slice(2) }))); assert.equal(g.length, 2); const body = issueBody(g.find((x) => x.keys.length === 2)); assert.match(body, //); assert.ok(hasMarker([body], ['b'])); assert.ok(!hasMarker([body], ['zzz'])); }); 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')); });