Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ if (parsedArgs) {
advisorySource.cleanup();
} catch (error) {
if (options.offline || options.offlineDb) {
throw new Error(`Offline advisory database is not available: ${error instanceof Error ? error.message : String(error)}`);
const reason = error instanceof Error ? error.message : String(error);
const syncHint = options.offlineDb
? `To build it, run: cve-lite advisories sync\nOr to save it to the requested path: cve-lite advisories sync --output ${options.offlineDb}`
: "To build it, run: cve-lite advisories sync";
throw new Error(`Offline advisory database is not available: ${reason}\n${syncHint}`);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string "To build it, run: cve-lite advisories sync" appears in both branches — it's the full else value and also the opening line of the if branch. If the command ever changes, it needs updating in two places. Worth extracting to a constant:

const BASE_SYNC_HINT = "To build it, run: cve-lite advisories sync";
const syncHint = options.offlineDb
  ? `${BASE_SYNC_HINT}\nOr to save it to the requested path: cve-lite advisories sync --output ${options.offlineDb}`
  : BASE_SYNC_HINT;

}
throw error;
}
Expand Down
51 changes: 51 additions & 0 deletions tests/cli-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,57 @@ describe("CLI integration", () => {
expect(stripAnsi(result.stdout[2] ?? "")).toContain("Advisory DB freshness: synced");
});

it("prints the advisories sync hint when the offline DB cannot be opened", async () => {
const createAdvisorySourceMock = (await import("../src/scanner.js")).createAdvisorySource as jest.Mock;
createAdvisorySourceMock.mockImplementationOnce(() => {
throw new Error("file does not exist");
});

parseArgsMock.mockReturnValue({
command: "scan",
options: {
offline: true,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four options failOn, batchSize, searchDepth, minSeverity are identical across both tests. A shared object at the top of the block makes it obvious which option is actually under test in each case:

const BASE_SCAN_OPTIONS = {
  failOn: "critical",
  batchSize: "100",
  searchDepth: "4",
  minSeverity: "medium",
} as const;

// then per test:
options: { offline: true, ...BASE_SCAN_OPTIONS }
options: { offlineDb: "/tmp/custom-advisories.db", ...BASE_SCAN_OPTIONS }

failOn: "critical",
batchSize: "100",
searchDepth: "4",
minSeverity: "medium",
},
projectArg: ".",
});

const result = await runIndexModule();

expect(result.exitCode).toBe(1);
expect(stripAnsi(result.stderr.join("\n"))).toContain("Offline advisory database is not available: file does not exist");
expect(stripAnsi(result.stderr.join("\n"))).toContain("To build it, run: cve-lite advisories sync");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stripAnsi(result.stderr.join("\n")) is evaluated twice here (and again in the second test). Store it once so the assertions read more cleanly:

const stderr = stripAnsi(result.stderr.join("\n"));
expect(stderr).toContain("Offline advisory database is not available: file does not exist");
expect(stderr).toContain("To build it, run: cve-lite advisories sync");

});

it("prints the requested output path when a custom offline DB cannot be opened", async () => {
const createAdvisorySourceMock = (await import("../src/scanner.js")).createAdvisorySource as jest.Mock;
createAdvisorySourceMock.mockImplementationOnce(() => {
throw new Error("permission denied");
});

parseArgsMock.mockReturnValue({
command: "scan",
options: {
offlineDb: "/tmp/custom-advisories.db",
failOn: "critical",
batchSize: "100",
searchDepth: "4",
minSeverity: "medium",
},
projectArg: ".",
});

const result = await runIndexModule();

expect(result.exitCode).toBe(1);
expect(stripAnsi(result.stderr.join("\n"))).toContain("Offline advisory database is not available: permission denied");
expect(stripAnsi(result.stderr.join("\n"))).toContain("To build it, run: cve-lite advisories sync");
expect(stripAnsi(result.stderr.join("\n"))).toContain("cve-lite advisories sync --output /tmp/custom-advisories.db");
});

it("warns when the local advisory DB appears stale", async () => {
const createAdvisorySourceMock = (await import("../src/scanner.js")).createAdvisorySource as jest.Mock;
createAdvisorySourceMock.mockReturnValueOnce({
Expand Down