mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(cli): reject negative and fractional count/pagination flags in Danish portal CLIs (#191)
Bare z.coerce.number() accepted negative and fractional values for count/pagination flags, and slice(0, limit) with a negative limit silently dropped trailing results instead of erroring. Tightens the schemas to .int().min(1) across all four Danish portal CLIs (including jobnet occupations --per-page) with network-free validation tests. By @ayobamiseun.
This commit is contained in:
@@ -10,7 +10,7 @@ export const search = defineCommand({
|
||||
short: "q",
|
||||
description: "Keyword search query (e.g. python, grafisk designer)",
|
||||
}),
|
||||
page: option(z.coerce.number().default(1), {
|
||||
page: option(z.coerce.number().int().min(1).default(1), {
|
||||
description: "Page number (1-indexed)",
|
||||
}),
|
||||
jobage: option(z.coerce.number().default(9999), {
|
||||
@@ -19,7 +19,7 @@ export const search = defineCommand({
|
||||
sort: option(z.string().default("score"), {
|
||||
description: "Sort order: score (relevance) or date (newest first)",
|
||||
}),
|
||||
limit: option(z.coerce.number().optional(), {
|
||||
limit: option(z.coerce.number().int().min(1).optional(), {
|
||||
description: "Cap total results returned by the CLI (client-side)",
|
||||
}),
|
||||
format: option(z.enum(["json", "table", "plain"]).default("json"), {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { runCLI } from "./helpers";
|
||||
|
||||
// All cases fail schema validation (or the required-flag guard) before any
|
||||
// network request, so the suite is network-free. Regression context: a bare
|
||||
// z.coerce.number() accepted --limit=-1, and slice(0, -1) then silently
|
||||
// dropped the last result instead of erroring.
|
||||
|
||||
function expectValidationError(result: { exitCode: number; stdout: string; stderr: string }, option: string) {
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
const error = JSON.parse(result.stderr);
|
||||
expect(error.ok).toBe(false);
|
||||
expect(error.error.kind).toBe("validation");
|
||||
expect(error.error.option).toBe(option);
|
||||
}
|
||||
|
||||
describe("Jobindex CLI flag validation", () => {
|
||||
test("--limit=-1 is rejected instead of silently dropping the last result", async () => {
|
||||
const result = await runCLI(["search", "--query", "test", "--limit=-1"]);
|
||||
expectValidationError(result, "limit");
|
||||
expect(JSON.parse(result.stderr).error.message).toContain("greater than or equal to 1");
|
||||
});
|
||||
|
||||
test("--limit=0 is rejected", async () => {
|
||||
const result = await runCLI(["search", "--query", "test", "--limit=0"]);
|
||||
expectValidationError(result, "limit");
|
||||
});
|
||||
|
||||
test("--limit=1.5 is rejected as non-integer", async () => {
|
||||
const result = await runCLI(["search", "--query", "test", "--limit=1.5"]);
|
||||
expectValidationError(result, "limit");
|
||||
expect(JSON.parse(result.stderr).error.message).toContain("Expected integer");
|
||||
});
|
||||
|
||||
test("--page=0 is rejected on the 1-indexed portal", async () => {
|
||||
const result = await runCLI(["search", "--query", "test", "--page=0"]);
|
||||
expectValidationError(result, "page");
|
||||
});
|
||||
|
||||
test("valid numeric flags pass schema validation (proven offline via the required-flag guard)", async () => {
|
||||
const result = await runCLI(["search", "--page=2", "--limit=5"]);
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(JSON.parse(result.stderr)).toEqual({
|
||||
error: "--query is required",
|
||||
code: "MISSING_REQUIRED",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user