From eef9c47461da6567ea26f3eb420ced06d75be9a4 Mon Sep 17 00:00:00 2001 From: Oscar Madera <80536682+oscarbol09@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:27:43 -0500 Subject: [PATCH] fix(cli): reject negative and fractional filter flags in Danish portal CLIs (#281) Follow-up to #191: it tightened page/limit/per-page, but five filter flags still used bare z.coerce.number() and accepted negative and fractional values that were sent raw to the portals (e.g. --jobage=-5, --radius=2.5). jobindex --jobage, jobnet --radius, jobdanmark --category/--jobtitle-id and jobbank --company now use .int().min(1), mirroring #191. Adds 8 network-free regression tests (a negative and a fractional case per flag) using the same validation-error pattern as the existing cli-flag-validation suites. --- .../jobbank-search/cli/src/commands/search.ts | 2 +- .../cli/tests/cli-flag-validation.test.ts | 16 +++++++++++++++- .../jobdanmark-search/cli/src/commands/search.ts | 4 ++-- .../cli/tests/cli-flag-validation.test.ts | 16 +++++++++++++++- .../jobindex-search/cli/src/commands/search.ts | 2 +- .../cli/tests/cli-flag-validation.test.ts | 15 ++++++++++++++- .../jobnet-search/cli/src/commands/search.ts | 2 +- .../cli/tests/cli-flag-validation.test.ts | 16 +++++++++++++++- 8 files changed, 64 insertions(+), 9 deletions(-) diff --git a/.agents/skills/jobbank-search/cli/src/commands/search.ts b/.agents/skills/jobbank-search/cli/src/commands/search.ts index f6b7ed1..c0981af 100644 --- a/.agents/skills/jobbank-search/cli/src/commands/search.ts +++ b/.agents/skills/jobbank-search/cli/src/commands/search.ts @@ -30,7 +30,7 @@ export const search = defineCommand({ "suitable-for": option(z.union([z.string(), z.array(z.string())]).optional(), { description: "Suitable-for code (andet). Repeatable.", }), - company: option(z.coerce.number().optional(), { + company: option(z.coerce.number().int().min(1).optional(), { description: "Company ID (virk)", }), remote: option(z.string().optional(), { diff --git a/.agents/skills/jobbank-search/cli/tests/cli-flag-validation.test.ts b/.agents/skills/jobbank-search/cli/tests/cli-flag-validation.test.ts index 30b68b6..2dadadd 100644 --- a/.agents/skills/jobbank-search/cli/tests/cli-flag-validation.test.ts +++ b/.agents/skills/jobbank-search/cli/tests/cli-flag-validation.test.ts @@ -4,7 +4,9 @@ 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. +// dropped the last result instead of erroring. The --company filter flag +// also accepted negative and fractional values that were sent raw to the +// portal. function expectValidationError(result: { exitCode: number; stdout: string; stderr: string }, option: string) { expect(result.exitCode).toBe(1); @@ -33,6 +35,18 @@ describe("Jobbank CLI flag validation", () => { expect(JSON.parse(result.stderr).error.message).toContain("Expected integer"); }); + test("search --company=-1 is rejected", async () => { + const result = await runCLI(["search", "--key", "test", "--company=-1"]); + expectValidationError(result, "company"); + expect(JSON.parse(result.stderr).error.message).toContain("greater than or equal to 1"); + }); + + test("search --company=1.5 is rejected as non-integer", async () => { + const result = await runCLI(["search", "--key", "test", "--company=1.5"]); + expectValidationError(result, "company"); + expect(JSON.parse(result.stderr).error.message).toContain("Expected integer"); + }); + test("valid --limit passes schema validation (proven offline via the required-filter guard)", async () => { const result = await runCLI(["search", "--limit=5"]); diff --git a/.agents/skills/jobdanmark-search/cli/src/commands/search.ts b/.agents/skills/jobdanmark-search/cli/src/commands/search.ts index 7cec7f4..d19969a 100644 --- a/.agents/skills/jobdanmark-search/cli/src/commands/search.ts +++ b/.agents/skills/jobdanmark-search/cli/src/commands/search.ts @@ -87,10 +87,10 @@ export const search = defineCommand({ text: option(z.string().optional(), { description: "Free-text keyword search (job title, keyword)", }), - category: option(z.coerce.number().optional(), { + category: option(z.coerce.number().int().min(1).optional(), { description: "Category ID", }), - "jobtitle-id": option(z.coerce.number().optional(), { + "jobtitle-id": option(z.coerce.number().int().min(1).optional(), { description: "Job title ID from autocomplete results", }), municipality: option(z.string().optional(), { diff --git a/.agents/skills/jobdanmark-search/cli/tests/cli-flag-validation.test.ts b/.agents/skills/jobdanmark-search/cli/tests/cli-flag-validation.test.ts index b3b8816..454c44b 100644 --- a/.agents/skills/jobdanmark-search/cli/tests/cli-flag-validation.test.ts +++ b/.agents/skills/jobdanmark-search/cli/tests/cli-flag-validation.test.ts @@ -4,7 +4,9 @@ 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. +// dropped the last result instead of erroring. Filter flags (--category, +// --jobtitle-id) also accepted negative and fractional values that were +// sent raw to the portal. function expectValidationError(result: { exitCode: number; stdout: string; stderr: string }, option: string) { expect(result.exitCode).toBe(1); @@ -27,6 +29,18 @@ describe("Jobdanmark CLI flag validation", () => { expectValidationError(result, "page"); }); + test("search --category=-1 is rejected", async () => { + const result = await runCLI(["search", "--category=-1"]); + expectValidationError(result, "category"); + expect(JSON.parse(result.stderr).error.message).toContain("greater than or equal to 1"); + }); + + test("search --jobtitle-id=1.5 is rejected as non-integer", async () => { + const result = await runCLI(["search", "--jobtitle-id=1.5"]); + expectValidationError(result, "jobtitle-id"); + expect(JSON.parse(result.stderr).error.message).toContain("Expected integer"); + }); + test("search --limit=1.5 is rejected as non-integer", async () => { const result = await runCLI(["search", "--limit=1.5"]); expectValidationError(result, "limit"); diff --git a/.agents/skills/jobindex-search/cli/src/commands/search.ts b/.agents/skills/jobindex-search/cli/src/commands/search.ts index 534bdbe..4e014c6 100644 --- a/.agents/skills/jobindex-search/cli/src/commands/search.ts +++ b/.agents/skills/jobindex-search/cli/src/commands/search.ts @@ -13,7 +13,7 @@ export const search = defineCommand({ page: option(z.coerce.number().int().min(1).default(1), { description: "Page number (1-indexed)", }), - jobage: option(z.coerce.number().default(9999), { + jobage: option(z.coerce.number().int().min(1).default(9999), { description: "Max age of posting in days: 1, 7, 14, 30, or 9999 (all)", }), sort: option(z.string().default("score"), { diff --git a/.agents/skills/jobindex-search/cli/tests/cli-flag-validation.test.ts b/.agents/skills/jobindex-search/cli/tests/cli-flag-validation.test.ts index 9d251fe..3cf10e3 100644 --- a/.agents/skills/jobindex-search/cli/tests/cli-flag-validation.test.ts +++ b/.agents/skills/jobindex-search/cli/tests/cli-flag-validation.test.ts @@ -4,7 +4,8 @@ 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. +// dropped the last result instead of erroring. Filter flags (--jobage) also +// accepted negative and fractional values that were sent raw to the portal. function expectValidationError(result: { exitCode: number; stdout: string; stderr: string }, option: string) { expect(result.exitCode).toBe(1); @@ -38,6 +39,18 @@ describe("Jobindex CLI flag validation", () => { expectValidationError(result, "page"); }); + test("--jobage=-5 is rejected", async () => { + const result = await runCLI(["search", "--query", "test", "--jobage=-5"]); + expectValidationError(result, "jobage"); + expect(JSON.parse(result.stderr).error.message).toContain("greater than or equal to 1"); + }); + + test("--jobage=1.5 is rejected as non-integer", async () => { + const result = await runCLI(["search", "--query", "test", "--jobage=1.5"]); + expectValidationError(result, "jobage"); + expect(JSON.parse(result.stderr).error.message).toContain("Expected integer"); + }); + test("valid numeric flags pass schema validation (proven offline via the required-flag guard)", async () => { const result = await runCLI(["search", "--page=2", "--limit=5"]); diff --git a/.agents/skills/jobnet-search/cli/src/commands/search.ts b/.agents/skills/jobnet-search/cli/src/commands/search.ts index 36a072b..e06c6ed 100644 --- a/.agents/skills/jobnet-search/cli/src/commands/search.ts +++ b/.agents/skills/jobnet-search/cli/src/commands/search.ts @@ -155,7 +155,7 @@ export const search = defineCommand({ "postal-code": option(z.string().optional(), { description: "Postal code for radius search", }), - radius: option(z.coerce.number().default(50), { + radius: option(z.coerce.number().int().min(1).default(50), { description: "Radius in km from postal code", }), "occupation-area": option(z.string().optional(), { diff --git a/.agents/skills/jobnet-search/cli/tests/cli-flag-validation.test.ts b/.agents/skills/jobnet-search/cli/tests/cli-flag-validation.test.ts index 6b2faad..40f5c74 100644 --- a/.agents/skills/jobnet-search/cli/tests/cli-flag-validation.test.ts +++ b/.agents/skills/jobnet-search/cli/tests/cli-flag-validation.test.ts @@ -4,7 +4,9 @@ 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 / --per-page=-1, and slice(0, -1) -// then silently dropped the last result instead of erroring. +// then silently dropped the last result instead of erroring. The --radius +// filter flag also accepted negative and fractional values that were sent +// raw to the portal. function expectValidationError(result: { exitCode: number; stdout: string; stderr: string }, option: string) { expect(result.exitCode).toBe(1); @@ -38,6 +40,18 @@ describe("Jobnet CLI flag validation", () => { expect(JSON.parse(result.stderr).error.message).toContain("Expected integer"); }); + test("search --radius=-10 is rejected", async () => { + const result = await runCLI(["search", "--radius=-10"]); + expectValidationError(result, "radius"); + expect(JSON.parse(result.stderr).error.message).toContain("greater than or equal to 1"); + }); + + test("search --radius=2.5 is rejected as non-integer", async () => { + const result = await runCLI(["search", "--radius=2.5"]); + expectValidationError(result, "radius"); + expect(JSON.parse(result.stderr).error.message).toContain("Expected integer"); + }); + test("occupations --per-page=-1 is rejected", async () => { const result = await runCLI(["occupations", "--per-page=-1"]); expectValidationError(result, "per-page");