Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

advisories.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

advisories.test.tsBlame195 lines · 1 contributor
f60ccdeClaude1/**
2 * Block J2 — Security advisory tests.
3 *
4 * Pure matcher tests + route auth smokes. Live scanning requires the DB
5 * and the J1 dep graph; those paths are exercised via integration.
6 */
7
8import { describe, it, expect } from "bun:test";
9import app from "../app";
10import {
11 SEED_ADVISORIES,
12 parseVersion,
13 compareVersions,
14 satisfiesRange,
15 normalizeManifestVersion,
16 rangeMatches,
17 __internal,
18} from "../lib/advisories";
19
20describe("advisories — parseVersion", () => {
21 it("parses dotted versions", () => {
22 expect(parseVersion("1.2.3")).toEqual([1, 2, 3]);
23 expect(parseVersion("0.5")).toEqual([0, 5]);
24 });
25
26 it("strips ^~ prefixes", () => {
27 expect(parseVersion("^1.2.3")).toEqual([1, 2, 3]);
28 expect(parseVersion("~2.0.0")).toEqual([2, 0, 0]);
29 expect(parseVersion("v1.8.0")).toEqual([1, 8, 0]);
30 });
31
32 it("strips prerelease suffixes", () => {
33 expect(parseVersion("1.2.3-beta.1")).toEqual([1, 2, 3]);
34 expect(parseVersion("1.2.3+build.99")).toEqual([1, 2, 3]);
35 });
36
37 it("treats non-numeric as 0", () => {
38 expect(parseVersion("garbage")).toEqual([0]);
39 expect(parseVersion("")).toEqual([0, 0, 0]);
40 });
41});
42
43describe("advisories — compareVersions", () => {
44 it("orders versions correctly", () => {
45 expect(compareVersions("1.2.3", "1.2.4")).toBeLessThan(0);
46 expect(compareVersions("1.2.4", "1.2.3")).toBeGreaterThan(0);
47 expect(compareVersions("1.2.3", "1.2.3")).toBe(0);
48 });
49
50 it("handles different length arrays", () => {
51 expect(compareVersions("1.2", "1.2.0")).toBe(0);
52 expect(compareVersions("1.2", "1.2.1")).toBeLessThan(0);
53 });
54
55 it("handles major bump across tens", () => {
56 expect(compareVersions("2.0.0", "10.0.0")).toBeLessThan(0);
57 expect(compareVersions("1.9.0", "1.10.0")).toBeLessThan(0);
58 });
59});
60
61describe("advisories — satisfiesRange", () => {
62 it("handles simple comparisons", () => {
63 expect(satisfiesRange("1.0.0", "<1.2.3")).toBe(true);
64 expect(satisfiesRange("1.2.3", "<1.2.3")).toBe(false);
65 expect(satisfiesRange("1.0.0", ">=1.0.0")).toBe(true);
66 expect(satisfiesRange("0.9.9", ">=1.0.0")).toBe(false);
67 });
68
69 it("handles compound ranges", () => {
70 expect(satisfiesRange("1.5.0", ">=1.0.0 <2.0.0")).toBe(true);
71 expect(satisfiesRange("0.9.0", ">=1.0.0 <2.0.0")).toBe(false);
72 expect(satisfiesRange("2.0.0", ">=1.0.0 <2.0.0")).toBe(false);
73 });
74
75 it("handles bare version as equality", () => {
76 expect(satisfiesRange("1.2.3", "1.2.3")).toBe(true);
77 expect(satisfiesRange("1.2.4", "1.2.3")).toBe(false);
78 });
79
80 it("handles <=, >=", () => {
81 expect(satisfiesRange("1.0.0", "<=1.0.0")).toBe(true);
82 expect(satisfiesRange("1.0.1", "<=1.0.0")).toBe(false);
83 expect(satisfiesRange("1.0.0", ">=1.0.0")).toBe(true);
84 });
85});
86
87describe("advisories — normalizeManifestVersion", () => {
88 it("strips semver prefixes", () => {
89 expect(normalizeManifestVersion("^1.2.3")).toBe("1.2.3");
90 expect(normalizeManifestVersion("~2.0.0")).toBe("2.0.0");
91 expect(normalizeManifestVersion("v1.8.0")).toBe("1.8.0");
92 });
93
94 it("plucks lower bound from compound spec", () => {
95 expect(normalizeManifestVersion(">=1.0 <2.0")).toBe("1.0");
96 });
97
98 it("returns null for unpinned / wildcard / null", () => {
99 expect(normalizeManifestVersion(null)).toBeNull();
100 expect(normalizeManifestVersion("*")).toBeNull();
101 expect(normalizeManifestVersion("latest")).toBeNull();
102 expect(normalizeManifestVersion("")).toBeNull();
103 });
104});
105
106describe("advisories — rangeMatches", () => {
107 it("matches unpatched version against <fixed range", () => {
108 expect(rangeMatches("4.17.10", "<4.17.12")).toBe(true);
109 expect(rangeMatches("^4.17.10", "<4.17.12")).toBe(true);
110 });
111
112 it("rejects patched version", () => {
113 expect(rangeMatches("4.17.21", "<4.17.12")).toBe(false);
114 expect(rangeMatches("^4.17.21", "<4.17.12")).toBe(false);
115 });
116
117 it("conservatively matches when spec can't be pinned", () => {
118 expect(rangeMatches("*", "<1.0.0")).toBe(true);
119 expect(rangeMatches(null, "<1.0.0")).toBe(true);
120 });
121
122 it("handles compound ranges from the seed list", () => {
123 // log4j CVE range
124 expect(rangeMatches("2.14.1", ">=2.0 <2.15.0")).toBe(true);
125 expect(rangeMatches("2.15.0", ">=2.0 <2.15.0")).toBe(false);
126 expect(rangeMatches("1.9.0", ">=2.0 <2.15.0")).toBe(false);
127 });
128});
129
130describe("advisories — seed data shape", () => {
131 it("has at least a dozen entries", () => {
132 expect(SEED_ADVISORIES.length).toBeGreaterThanOrEqual(10);
133 });
134
135 it("every entry has required fields", () => {
136 for (const a of SEED_ADVISORIES) {
137 expect(typeof a.ghsaId).toBe("string");
138 expect(a.ghsaId.startsWith("GHSA-")).toBe(true);
139 expect(["low", "moderate", "high", "critical"]).toContain(a.severity);
140 expect(typeof a.ecosystem).toBe("string");
141 expect(typeof a.packageName).toBe("string");
142 expect(typeof a.affectedRange).toBe("string");
143 }
144 });
145
146 it("log4j advisory is present", () => {
147 const log4j = SEED_ADVISORIES.find((a) => a.cveId === "CVE-2021-44228");
148 expect(log4j).toBeDefined();
149 expect(log4j!.severity).toBe("critical");
150 });
151
152 it("seed ghsa_ids are unique", () => {
153 const ids = SEED_ADVISORIES.map((a) => a.ghsaId);
154 expect(new Set(ids).size).toBe(ids.length);
155 });
156});
157
158describe("advisories — route auth", () => {
159 it("GET /:o/:r/security/advisories for unknown repo → 404 or 500", async () => {
160 const res = await app.request("/nobody/missing/security/advisories");
161 expect([404, 500]).toContain(res.status);
162 });
163
164 it("GET /all variant also mounted", async () => {
165 const res = await app.request("/nobody/missing/security/advisories/all");
166 expect([404, 500]).toContain(res.status);
167 });
168
169 it("POST scan without auth → 302 /login", async () => {
170 const res = await app.request(
171 "/alice/repo/security/advisories/scan",
172 { method: "POST" }
173 );
174 expect(res.status).toBe(302);
175 expect(res.headers.get("location") || "").toContain("/login");
176 });
177
178 it("POST dismiss without auth → 302 /login", async () => {
179 const res = await app.request(
180 "/alice/repo/security/advisories/00000000-0000-0000-0000-000000000000/dismiss",
181 { method: "POST" }
182 );
183 expect(res.status).toBe(302);
184 expect(res.headers.get("location") || "").toContain("/login");
185 });
186
187 it("POST reopen without auth → 302 /login", async () => {
188 const res = await app.request(
189 "/alice/repo/security/advisories/00000000-0000-0000-0000-000000000000/reopen",
190 { method: "POST" }
191 );
192 expect(res.status).toBe(302);
193 expect(res.headers.get("location") || "").toContain("/login");
194 });
195});