|
| 1 | +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
| 2 | +import fs from 'fs'; |
| 3 | +import os from 'os'; |
| 4 | +import path from 'path'; |
| 5 | +import { runAsync, REPO, TIMEOUT } from './helpers.js'; |
| 6 | + |
| 7 | +// ── Shared setup ────────────────────────────────────────────────────────────── |
| 8 | +let tmpDir; |
| 9 | +let r = {}; // all pre-fetched results keyed by label |
| 10 | + |
| 11 | +beforeAll(async () => { |
| 12 | + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codeant-results-')); |
| 13 | + |
| 14 | + const sarifPath = path.join(tmpDir, 'out.sarif'); |
| 15 | + const jsonPath = path.join(tmpDir, 'out.json'); |
| 16 | + |
| 17 | + // Fire every CLI call in parallel — total wall time ≈ slowest single call |
| 18 | + const calls = { |
| 19 | + base: ['scans', 'results', '--repo', REPO, '--quiet'], |
| 20 | + scanSha: ['scans', 'history', '--repo', REPO, '--limit', '1'], |
| 21 | + typeSast: ['scans', 'results', '--repo', REPO, '--types', 'sast', '--quiet'], |
| 22 | + typeSca: ['scans', 'results', '--repo', REPO, '--types', 'sca', '--quiet'], |
| 23 | + typeSecrets: ['scans', 'results', '--repo', REPO, '--types', 'secrets', '--quiet'], |
| 24 | + typeIac: ['scans', 'results', '--repo', REPO, '--types', 'iac', '--quiet'], |
| 25 | + typeDeadCode: ['scans', 'results', '--repo', REPO, '--types', 'dead_code', '--quiet'], |
| 26 | + typeSastSca: ['scans', 'results', '--repo', REPO, '--types', 'sast,sca', '--quiet'], |
| 27 | + severity: ['scans', 'results', '--repo', REPO, '--severity', 'critical,high', '--quiet'], |
| 28 | + page0: ['scans', 'results', '--repo', REPO, '--limit', '5', '--offset', '0', '--quiet'], |
| 29 | + page1: ['scans', 'results', '--repo', REPO, '--limit', '5', '--offset', '5', '--quiet'], |
| 30 | + pathFilter: ['scans', 'results', '--repo', REPO, '--path', 'src/**', '--quiet'], |
| 31 | + checkFilter: ['scans', 'results', '--repo', REPO, '--check', '(?i)sql', '--quiet'], |
| 32 | + fmtJson: ['scans', 'results', '--repo', REPO, '--format', 'json', '--quiet'], |
| 33 | + fmtCsv: ['scans', 'results', '--repo', REPO, '--format', 'csv', '--quiet'], |
| 34 | + fmtMd: ['scans', 'results', '--repo', REPO, '--format', 'md', '--quiet'], |
| 35 | + fmtTable: ['scans', 'results', '--repo', REPO, '--format', 'table', '--no-color', '--quiet'], |
| 36 | + fmtSarif: ['scans', 'results', '--repo', REPO, '--format', 'sarif', '--output', sarifPath, '--quiet'], |
| 37 | + fmtJsonFile: ['scans', 'results', '--repo', REPO, '--output', jsonPath, '--quiet'], |
| 38 | + fieldsBasic: ['scans', 'results', '--repo', REPO, '--fields', 'id,severity,file_path', '--quiet'], |
| 39 | + fieldsProjected: ['scans', 'results', '--repo', REPO, '--fields', 'id,severity', '--limit', '1', '--quiet'], |
| 40 | + includeDismissed:['scans', 'results', '--repo', REPO, '--include-dismissed', '--quiet'], |
| 41 | + deterA: ['scans', 'results', '--repo', REPO, '--types', 'sast', '--limit', '20', '--quiet'], |
| 42 | + deterB: ['scans', 'results', '--repo', REPO, '--types', 'sast', '--limit', '20', '--quiet'], |
| 43 | + }; |
| 44 | + |
| 45 | + const keys = Object.keys(calls); |
| 46 | + const results = await Promise.all(keys.map((k) => runAsync(...calls[k]))); |
| 47 | + keys.forEach((k, i) => { r[k] = results[i]; }); |
| 48 | + |
| 49 | + // Extract scan SHA from history for the explicit-SHA test |
| 50 | + const shaMatch = r.scanSha.stdout.match(/"latest_commit_sha":\s*"([^"]+)"/); |
| 51 | + r._scanSha = shaMatch ? shaMatch[1] : ''; |
| 52 | + r._sarifPath = sarifPath; |
| 53 | + r._jsonPath = jsonPath; |
| 54 | +}, TIMEOUT * 3); |
| 55 | + |
| 56 | +afterAll(() => { |
| 57 | + fs.rmSync(tmpDir, { recursive: true, force: true }); |
| 58 | +}); |
| 59 | + |
| 60 | +// ── Basic ───────────────────────────────────────────────────────────────────── |
| 61 | +describe('results — basic', () => { |
| 62 | + it('exit 0', () => expect(r.base.status, r.base.combined).toBe(0)); |
| 63 | + it('schema_version 1.0', () => expect(r.base.stdout).toMatch('"schema_version": "1.0"')); |
| 64 | + it("has 'findings' key", () => expect(r.base.stdout).toMatch('"findings"')); |
| 65 | + it("has 'pagination' key",() => expect(r.base.stdout).toMatch('"pagination"')); |
| 66 | +}); |
| 67 | + |
| 68 | +// ── Type filters ────────────────────────────────────────────────────────────── |
| 69 | +describe('results — type filters', () => { |
| 70 | + for (const [label, key] of [ |
| 71 | + ['sast', 'typeSast'], ['sca', 'typeSca'], ['secrets', 'typeSecrets'], |
| 72 | + ['iac', 'typeIac'], ['dead_code', 'typeDeadCode'], ['sast,sca', 'typeSastSca'], |
| 73 | + ]) { |
| 74 | + it(`--types ${label} — exit 0`, () => expect(r[key].status, r[key].combined).toBe(0)); |
| 75 | + } |
| 76 | +}); |
| 77 | + |
| 78 | +// ── Severity + pagination ───────────────────────────────────────────────────── |
| 79 | +describe('results — severity + pagination', () => { |
| 80 | + it('--severity critical,high — exit 0', () => expect(r.severity.status, r.severity.combined).toBe(0)); |
| 81 | + it('--limit 5 --offset 0 — exit 0', () => expect(r.page0.status, r.page0.combined).toBe(0)); |
| 82 | + it('--limit 5 --offset 5 — exit 0', () => expect(r.page1.status, r.page1.combined).toBe(0)); |
| 83 | +}); |
| 84 | + |
| 85 | +// ── Path + check filters ────────────────────────────────────────────────────── |
| 86 | +describe('results — path + check filters', () => { |
| 87 | + it("--path 'src/**' — exit 0", () => expect(r.pathFilter.status, r.pathFilter.combined).toBe(0)); |
| 88 | + it("--check '(?i)sql' — exit 0", () => expect(r.checkFilter.status, r.checkFilter.combined).toBe(0)); |
| 89 | +}); |
| 90 | + |
| 91 | +// ── Formats ─────────────────────────────────────────────────────────────────── |
| 92 | +describe('results — formats', () => { |
| 93 | + it('--format json — exit 0', () => expect(r.fmtJson.status, r.fmtJson.combined).toBe(0)); |
| 94 | + it('--format csv — exit 0', () => expect(r.fmtCsv.status, r.fmtCsv.combined).toBe(0)); |
| 95 | + it('--format md — exit 0', () => expect(r.fmtMd.status, r.fmtMd.combined).toBe(0)); |
| 96 | + it('--format table — exit 0', () => expect(r.fmtTable.status, r.fmtTable.combined).toBe(0)); |
| 97 | + |
| 98 | + it('csv — has header row', () => expect(r.fmtCsv.combined).toMatch('id,category')); |
| 99 | + it('md — has CodeAnt Scan heading', () => expect(r.fmtMd.combined).toMatch('CodeAnt Scan')); |
| 100 | + it('table — has SEVERITY column',() => expect(r.fmtTable.combined).toMatch('SEVERITY')); |
| 101 | +}); |
| 102 | + |
| 103 | +// ── SARIF output to file ────────────────────────────────────────────────────── |
| 104 | +describe('results — SARIF output to file', () => { |
| 105 | + it('command exits 0', () => expect(r.fmtSarif.status, r.fmtSarif.combined).toBe(0)); |
| 106 | + it('SARIF file exists', () => expect(fs.existsSync(r._sarifPath)).toBe(true)); |
| 107 | + it('SARIF has version 2.1.0', () => expect(fs.readFileSync(r._sarifPath, 'utf8')).toMatch('"version": "2.1.0"')); |
| 108 | + it('SARIF has runs array', () => expect(JSON.parse(fs.readFileSync(r._sarifPath, 'utf8'))).toHaveProperty('runs')); |
| 109 | +}); |
| 110 | + |
| 111 | +// ── JSON output to file ─────────────────────────────────────────────────────── |
| 112 | +describe('results — JSON output to file', () => { |
| 113 | + it('JSON file exists', () => expect(fs.existsSync(r._jsonPath)).toBe(true)); |
| 114 | + it('JSON file schema_version 1.0', () => expect(JSON.parse(fs.readFileSync(r._jsonPath, 'utf8')).schema_version).toBe('1.0')); |
| 115 | +}); |
| 116 | + |
| 117 | +// ── Field projection ────────────────────────────────────────────────────────── |
| 118 | +describe('results — field projection', () => { |
| 119 | + it('--fields id,severity,file_path — exit 0', () => expect(r.fieldsBasic.status, r.fieldsBasic.combined).toBe(0)); |
| 120 | + |
| 121 | + it('projected output only includes requested keys', () => { |
| 122 | + let parsed; |
| 123 | + try { parsed = JSON.parse(r.fieldsProjected.stdout); } catch { return; } |
| 124 | + const findings = parsed?.findings ?? []; |
| 125 | + if (findings.length === 0) return; |
| 126 | + const keys = Object.keys(findings[0]); |
| 127 | + expect(keys).toContain('id'); |
| 128 | + expect(keys).not.toContain('message'); |
| 129 | + }); |
| 130 | +}); |
| 131 | + |
| 132 | +// ── Dismissed ───────────────────────────────────────────────────────────────── |
| 133 | +describe('results — dismissed', () => { |
| 134 | + it('default excludes dismissed — exit 0', () => expect(r.base.status, r.base.combined).toBe(0)); |
| 135 | + it('--include-dismissed — exit 0', () => expect(r.includeDismissed.status, r.includeDismissed.combined).toBe(0)); |
| 136 | +}); |
| 137 | + |
| 138 | +// ── Explicit scan SHA ───────────────────────────────────────────────────────── |
| 139 | +describe('results — explicit scan SHA', () => { |
| 140 | + it('--scan <sha> — exit 0', async () => { |
| 141 | + if (!r._scanSha) return; |
| 142 | + const out = await runAsync('scans', 'results', '--repo', REPO, '--scan', r._scanSha, '--quiet'); |
| 143 | + expect(out.status, out.combined).toBe(0); |
| 144 | + }, TIMEOUT); |
| 145 | +}); |
| 146 | + |
| 147 | +// ── Quiet ───────────────────────────────────────────────────────────────────── |
| 148 | +describe('results — quiet', () => { |
| 149 | + it('--quiet suppresses [progress] lines', () => expect(r.base.stderr).not.toMatch(/\[progress\]/)); |
| 150 | +}); |
| 151 | + |
| 152 | +// ── Determinism ─────────────────────────────────────────────────────────────── |
| 153 | +describe('results — determinism', () => { |
| 154 | + it('two runs produce identical output (excluding generated_at)', () => { |
| 155 | + const strip = (s) => s.replace(/"generated_at":\s*"[^"]*"/g, '"generated_at": "<stripped>"'); |
| 156 | + expect(strip(r.deterA.stdout)).toBe(strip(r.deterB.stdout)); |
| 157 | + }); |
| 158 | +}); |
0 commit comments