Skip to content

Commit 52ef3f7

Browse files
committed
adding tests
1 parent 0d9fc5d commit 52ef3f7

8 files changed

Lines changed: 380 additions & 0 deletions

File tree

‎tests/scans/dismissed.test.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { run, REPO, TIMEOUT } from './helpers.js';
3+
4+
describe('dismissed', () => {
5+
let outBasic;
6+
let outSecrets;
7+
8+
beforeAll(() => {
9+
outBasic = run('scans', 'dismissed', '--repo', REPO);
10+
outSecrets = run('scans', 'dismissed', '--repo', REPO, '--analysis-type', 'secrets');
11+
});
12+
13+
it('--repo — exit 0', () => {
14+
expect(outBasic.status).toBe(0);
15+
}, TIMEOUT);
16+
17+
it('--analysis-type secrets — graceful exit (0 or 1)', () => {
18+
expect(outSecrets.status).toBeLessThanOrEqual(1);
19+
}, TIMEOUT);
20+
21+
it("output has 'dismissed_alerts' key", () => {
22+
expect(outBasic.combined).toMatch('dismissed_alerts');
23+
}, TIMEOUT);
24+
});

‎tests/scans/errors.test.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { run, REPO, TIMEOUT } from './helpers.js';
3+
4+
describe('error handling', () => {
5+
let outBogusType;
6+
let outBadRepo;
7+
8+
beforeAll(() => {
9+
outBogusType = run('scans', 'results', '--repo', REPO, '--types', 'bogus_type');
10+
outBadRepo = run('scans', 'results', '--repo', 'NONEXISTENT_ORG_XYZ/NONEXISTENT_REPO_XYZ', '--quiet');
11+
});
12+
13+
it('unknown --types exits non-zero', () => {
14+
expect(outBogusType.status).not.toBe(0);
15+
}, TIMEOUT);
16+
17+
it('unknown --types shows valid type list', () => {
18+
expect(outBogusType.combined).toMatch(/valid|Valid|sast/i);
19+
}, TIMEOUT);
20+
21+
it('nonexistent repo exits non-zero', () => {
22+
expect(outBadRepo.status).not.toBe(0);
23+
}, TIMEOUT);
24+
});

‎tests/scans/get.test.js‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { run, REPO, TIMEOUT } from './helpers.js';
3+
4+
describe('get', () => {
5+
let outBasic;
6+
let outTypes;
7+
8+
beforeAll(() => {
9+
outBasic = run('scans', 'get', '--repo', REPO, '--quiet');
10+
outTypes = run('scans', 'get', '--repo', REPO, '--types', 'sast,sca', '--quiet');
11+
});
12+
13+
it('latest scan summary — exit 0', () => {
14+
expect(outBasic.status).toBe(0);
15+
}, TIMEOUT);
16+
17+
it('--types sast,sca — exit 0', () => {
18+
expect(outTypes.status).toBe(0);
19+
}, TIMEOUT);
20+
21+
it("output has 'summary' key", () => {
22+
expect(outBasic.combined).toMatch('summary');
23+
}, TIMEOUT);
24+
25+
it('--quiet suppresses stderr progress', () => {
26+
expect(outBasic.stderr).not.toMatch(/\[progress\]/);
27+
}, TIMEOUT);
28+
});

‎tests/scans/helpers.js‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { spawnSync, execFile } from 'child_process';
2+
import { promisify } from 'util';
3+
import { expect } from 'vitest';
4+
5+
const execFileAsync = promisify(execFile);
6+
7+
export const REPO = process.env.REPO ?? 'CodeAnt-AI/codeant-ci-backend-2';
8+
export const ORG = process.env.ORG ?? REPO.split('/')[0];
9+
export const TIMEOUT = 60_000;
10+
11+
const CLI = ['node', 'src/index.js'];
12+
13+
/** Run a CLI command synchronously; return { status, stdout, stderr, combined }. */
14+
export function run(...args) {
15+
const result = spawnSync(CLI[0], [...CLI.slice(1), ...args], {
16+
encoding: 'utf8',
17+
timeout: TIMEOUT - 5_000,
18+
env: { ...process.env },
19+
input: '',
20+
stdio: ['pipe', 'pipe', 'pipe'],
21+
});
22+
return {
23+
status: result.status ?? 1,
24+
signal: result.signal ?? null,
25+
stdout: result.stdout ?? '',
26+
stderr: result.stderr ?? '',
27+
combined: result.signal
28+
? `[killed: ${result.signal}]`
29+
: (result.stdout ?? '') + (result.stderr ?? ''),
30+
};
31+
}
32+
33+
/**
34+
* Run a CLI command asynchronously.
35+
* Use with Promise.all() to fire multiple calls in parallel.
36+
* Returns the same { status, stdout, stderr, combined } shape as run().
37+
*/
38+
export async function runAsync(...args) {
39+
try {
40+
const { stdout, stderr } = await execFileAsync(CLI[0], [...CLI.slice(1), ...args], {
41+
encoding: 'utf8',
42+
timeout: TIMEOUT - 5_000,
43+
env: { ...process.env },
44+
maxBuffer: 100 * 1024 * 1024, // 100 MB
45+
});
46+
return { status: 0, stdout, stderr, combined: stdout + stderr };
47+
} catch (err) {
48+
const stdout = err.stdout ?? '';
49+
const stderr = err.stderr ?? '';
50+
return { status: err.code ?? 1, stdout, stderr, combined: stdout + stderr };
51+
}
52+
}
53+
54+
/** Assert exit 0 and return result. */
55+
export function expectOk(...args) {
56+
const r = run(...args);
57+
expect(r.status, `expected exit 0 for: ${args.join(' ')}\nstderr: ${r.stderr}`).toBe(0);
58+
return r;
59+
}
60+
61+
/** Assert exit matches expected (or any non-zero when expected === 'nonzero'). */
62+
export function expectExit(expected, ...args) {
63+
const r = run(...args);
64+
if (expected === 'nonzero') {
65+
expect(r.status, `expected non-zero exit for: ${args.join(' ')}`).not.toBe(0);
66+
} else {
67+
expect(r.status, `expected exit ${expected} for: ${args.join(' ')}\nstderr: ${r.stderr}`).toBe(expected);
68+
}
69+
return r;
70+
}
71+
72+
/** Assert combined output matches pattern. */
73+
export function expectOutput(pattern, ...args) {
74+
const r = run(...args);
75+
expect(r.combined, `expected "${pattern}" in output of: ${args.join(' ')}`).toMatch(pattern);
76+
return r;
77+
}

‎tests/scans/history.test.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { run, REPO, TIMEOUT } from './helpers.js';
3+
4+
describe('history', () => {
5+
let outBasic;
6+
let outLimit;
7+
let outBranch;
8+
9+
beforeAll(() => {
10+
outBasic = run('scans', 'history', '--repo', REPO);
11+
outLimit = run('scans', 'history', '--repo', REPO, '--limit', '3');
12+
outBranch = run('scans', 'history', '--repo', REPO, '--branch', 'main');
13+
});
14+
15+
it('basic — exit 0', () => {
16+
expect(outBasic.status).toBe(0);
17+
}, TIMEOUT);
18+
19+
it('--limit 3 — exit 0', () => {
20+
expect(outLimit.status).toBe(0);
21+
}, TIMEOUT);
22+
23+
it('--branch main — exit 0', () => {
24+
expect(outBranch.status).toBe(0);
25+
}, TIMEOUT);
26+
27+
it("output has 'scans' key", () => {
28+
expect(outBasic.combined).toMatch('scans');
29+
}, TIMEOUT);
30+
});

‎tests/scans/orgs.test.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { run, TIMEOUT } from './helpers.js';
3+
4+
describe('orgs', () => {
5+
let out;
6+
beforeAll(() => { out = run('scans', 'orgs'); });
7+
8+
it('returns exit 0', () => {
9+
expect(out.status).toBe(0);
10+
}, TIMEOUT);
11+
12+
it("output has 'connections' key", () => {
13+
expect(out.combined).toMatch('connections');
14+
}, TIMEOUT);
15+
});

‎tests/scans/repos.test.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { run, REPO, ORG, TIMEOUT } from './helpers.js';
3+
4+
describe('repos', () => {
5+
let outDefault;
6+
let outExplicit;
7+
8+
beforeAll(() => {
9+
outDefault = run('scans', 'repos');
10+
outExplicit = run('scans', 'repos', '--org', ORG);
11+
});
12+
13+
it('auto-pick org — exit 0', () => {
14+
expect(outDefault.status).toBe(0);
15+
}, TIMEOUT);
16+
17+
it('--org explicit — exit 0', () => {
18+
expect(outExplicit.status).toBe(0);
19+
}, TIMEOUT);
20+
21+
it("output has 'repos' key", () => {
22+
expect(outExplicit.combined).toMatch('repos');
23+
}, TIMEOUT);
24+
});

‎tests/scans/results.test.js‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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

Comments
 (0)