Files
2026-07-13 13:39:12 +08:00

328 lines
13 KiB
TypeScript

// tests/unit/build/check-codeql-ratchet.test.ts
// TDD unit tests for scripts/check/check-codeql-ratchet.mjs — Task 7.3 CodeQL ratchet.
//
// Strategy: test the exported pure function without calling the GitHub API.
// All fixtures are synthetic GitHub API responses.
// - parseCodeQLAlerts() — filters + counts open, non-dismissed CodeQL alerts
// (Hard Rule #14: dismissed alerts do not count)
import test from "node:test";
import assert from "node:assert/strict";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import {
parseCodeQLAlerts,
evaluateCodeqlRatchet,
} from "../../../scripts/check/check-codeql-ratchet.mjs";
type RatchetVerdict = { regressed: boolean; improved: boolean };
const evaluate = evaluateCodeqlRatchet as (current: number, baseline: number) => RatchetVerdict;
// ---------------------------------------------------------------------------
// Fixtures — synthetic GitHub code-scanning/alerts API responses
// ---------------------------------------------------------------------------
/** Helper to build a minimal alert object. */
function makeAlert(
overrides: {
number?: number;
state?: "open" | "dismissed" | "fixed";
tool?: string;
ruleId?: string;
severity?: string;
securitySeverity?: string;
dismissedReason?: string | null;
} = {}
) {
return {
number: overrides.number ?? 1,
state: overrides.state ?? "open",
dismissed_reason: overrides.dismissedReason ?? null,
dismissed_at: overrides.dismissedReason ? "2026-01-01T00:00:00Z" : null,
tool: {
name: overrides.tool ?? "CodeQL",
guid: null,
version: "2.16.0",
},
rule: {
id: overrides.ruleId ?? "js/sql-injection",
name: overrides.ruleId ?? "SQL Injection",
severity: overrides.severity ?? "error",
security_severity_level: overrides.securitySeverity ?? "high",
description: "Vulnerability description",
},
most_recent_instance: {
ref: "refs/heads/main",
state: overrides.state ?? "open",
},
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T00:00:00Z",
url: "https://api.github.com/repos/owner/repo/code-scanning/alerts/1",
html_url: "https://github.com/owner/repo/security/code-scanning/1",
};
}
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — input inválido
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: null retorna alertCount=0", () => {
const result = parseCodeQLAlerts(null);
assert.equal(result.alertCount, 0);
assert.deepEqual(result.bySeverity, {});
assert.deepEqual(result.byRule, {});
});
test("parseCodeQLAlerts: undefined retorna alertCount=0", () => {
const result = parseCodeQLAlerts(undefined as unknown as null);
assert.equal(result.alertCount, 0);
});
test("parseCodeQLAlerts: objeto (não-array) retorna alertCount=0", () => {
const result = parseCodeQLAlerts({ number: 1, state: "open" } as unknown as null);
assert.equal(result.alertCount, 0);
});
test("parseCodeQLAlerts: string retorna alertCount=0", () => {
const result = parseCodeQLAlerts("open" as unknown as null);
assert.equal(result.alertCount, 0);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — array vazio
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: array vazio retorna alertCount=0", () => {
const result = parseCodeQLAlerts([]);
assert.equal(result.alertCount, 0);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — Hard Rule #14: dismissed alerts don't count
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: alerta dismissed NÃO conta (Hard Rule #14)", () => {
const alerts = [makeAlert({ state: "dismissed", dismissedReason: "false positive" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "dismissed alert must not be counted");
});
test("parseCodeQLAlerts: alerta dismissed com razão 'used in tests' NÃO conta", () => {
const alerts = [makeAlert({ state: "dismissed", dismissedReason: "used in tests" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0);
});
test("parseCodeQLAlerts: alerta dismissed independente da razão NÃO conta", () => {
const alerts = [
makeAlert({ state: "dismissed", dismissedReason: "wont fix" }),
makeAlert({ number: 2, state: "dismissed", dismissedReason: "false positive" }),
makeAlert({ number: 3, state: "dismissed", dismissedReason: "used in tests" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "all dismissed states must be excluded");
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — estado fixed não conta
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: alerta fixed NÃO conta", () => {
const alerts = [makeAlert({ state: "fixed" as "open" | "dismissed" | "fixed" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "fixed alerts are not open, must not count");
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — somente alertas CodeQL (filtra outras ferramentas)
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: alertas de outras ferramentas são ignorados", () => {
const alerts = [
makeAlert({ tool: "Semgrep", ruleId: "semgrep-rule-1" }),
makeAlert({ number: 2, tool: "ESLint", ruleId: "eslint-rule-1" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "only CodeQL alerts should be counted");
});
test("parseCodeQLAlerts: tool 'CodeQL' (maiúsculas) conta", () => {
const alerts = [makeAlert({ tool: "CodeQL" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
test("parseCodeQLAlerts: tool 'codeql' (minúsculas) conta (case-insensitive)", () => {
const alerts = [makeAlert({ tool: "codeql" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
test("parseCodeQLAlerts: tool 'CodeQL Community' também conta (contém 'codeql')", () => {
const alerts = [makeAlert({ tool: "CodeQL Community" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — contagem de alertas open
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: 1 alerta open CodeQL retorna alertCount=1", () => {
const alerts = [makeAlert({ state: "open" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
test("parseCodeQLAlerts: 3 alertas open retorna alertCount=3", () => {
const alerts = [
makeAlert({ number: 1, state: "open" }),
makeAlert({ number: 2, state: "open", ruleId: "js/xss" }),
makeAlert({ number: 3, state: "open", ruleId: "js/path-injection" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 3);
});
test("parseCodeQLAlerts: mix de open, dismissed, fixed — conta só open", () => {
const alerts = [
makeAlert({ number: 1, state: "open" }),
makeAlert({ number: 2, state: "dismissed", dismissedReason: "false positive" }),
makeAlert({ number: 3, state: "fixed" as "open" | "dismissed" | "fixed" }),
makeAlert({ number: 4, state: "open", ruleId: "js/xss" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 2, "only 2 open, non-dismissed alerts");
});
test("parseCodeQLAlerts: mix de CodeQL e outras ferramentas — conta só CodeQL", () => {
const alerts = [
makeAlert({ number: 1, tool: "CodeQL", state: "open" }),
makeAlert({ number: 2, tool: "Semgrep", state: "open" }),
makeAlert({ number: 3, tool: "CodeQL", state: "open", ruleId: "js/xss" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 2, "only CodeQL open alerts");
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — severidade
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: coleta bySeverity de security_severity_level", () => {
const alerts = [
makeAlert({ number: 1, securitySeverity: "critical" }),
makeAlert({ number: 2, securitySeverity: "high" }),
makeAlert({ number: 3, securitySeverity: "medium" }),
makeAlert({ number: 4, securitySeverity: "high" }), // segundo high
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 4);
assert.equal(result.bySeverity["critical"], 1);
assert.equal(result.bySeverity["high"], 2);
assert.equal(result.bySeverity["medium"], 1);
});
test("parseCodeQLAlerts: alerta sem security_severity_level usa rule.severity", () => {
const alerts = [
{
...makeAlert({ number: 1 }),
rule: {
id: "js/unused-local-variable",
name: "Unused variable",
severity: "warning",
// sem security_severity_level
description: "Local var not used",
},
},
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
assert.ok(
"warning" in result.bySeverity || "unknown" in result.bySeverity,
"severity should be captured"
);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — byRule
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: agrupa por rule.id em byRule", () => {
const alerts = [
makeAlert({ number: 1, ruleId: "js/sql-injection" }),
makeAlert({ number: 2, ruleId: "js/xss" }),
makeAlert({ number: 3, ruleId: "js/sql-injection" }), // segundo do mesmo rule
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.byRule["js/sql-injection"], 2);
assert.equal(result.byRule["js/xss"], 1);
});
test("parseCodeQLAlerts: alerta sem rule retorna byRule com 'unknown'", () => {
const alert = {
number: 1,
state: "open",
dismissed_reason: null,
dismissed_at: null,
tool: { name: "CodeQL", guid: null, version: "2.16.0" },
// sem rule
};
const result = parseCodeQLAlerts([alert]);
assert.equal(result.alertCount, 1);
assert.ok("unknown" in result.byRule);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — dismissed não contamina contagens
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: dismissed com mesmo ruleId que open — dismissed não aparece em byRule", () => {
const alerts = [
makeAlert({ number: 1, state: "open", ruleId: "js/sql-injection" }),
makeAlert({
number: 2,
state: "dismissed",
ruleId: "js/sql-injection",
dismissedReason: "false positive",
}),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
assert.equal(result.byRule["js/sql-injection"], 1, "only the open alert should appear in byRule");
});
// ---------------------------------------------------------------------------
// evaluateCodeqlRatchet — ratchet direction:down (Task 7.3 promote to blocking)
// Mirror of evaluateDeadCode: regression when measured > baseline; the baseline
// is 0 (clean), so ANY open CodeQL alert is a regression that blocks.
// ---------------------------------------------------------------------------
test("evaluateCodeqlRatchet: equal to baseline passes (0 vs 0 — clean)", () => {
const r = evaluate(0, 0);
assert.equal(r.regressed, false);
assert.equal(r.improved, false);
});
test("evaluateCodeqlRatchet: one more alert than baseline 0 is a regression", () => {
const r = evaluate(1, 0);
assert.equal(r.regressed, true, "a single new open CodeQL alert must block");
assert.equal(r.improved, false);
});
test("evaluateCodeqlRatchet: fewer alerts than a non-zero baseline is an improvement", () => {
const r = evaluate(2, 5);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateCodeqlRatchet: zero alerts against a non-zero baseline is a maximum improvement", () => {
const r = evaluate(0, 5);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateCodeqlRatchet: strict integer comparison — any increase regresses", () => {
assert.equal(evaluate(6, 5).regressed, true);
assert.equal(evaluate(5, 5).regressed, false);
assert.equal(evaluate(4, 5).regressed, false);
});