fix(linkedin-search): reject fractional numeric flags (#371) (#393)

parseInt truncated values before validation, so --jobage 0.5 became 0 and silently omitted LinkedIn's freshness filter. Require whole numbers of at least 1 for every numeric search flag and guard the behavior with CLI regression tests.
This commit is contained in:
Ayobami Adegoke
2026-09-02 20:08:26 +02:00
committed by GitHub
parent 284dc4c2d0
commit c42806674b
3 changed files with 44 additions and 30 deletions
+9 -11
View File
@@ -126,9 +126,14 @@ async function main(): Promise<number> {
} }
const parseIntFlag = (name: string, raw: string | boolean | string[]): number | null => { const parseIntFlag = (name: string, raw: string | boolean | string[]): number | null => {
const val = parseInt(raw as string, 10) // Number(), not parseInt(): parseInt truncates, so "--jobage 0.5"
if (isNaN(val)) { // became 0 and silently dropped f_TPR from the request (#371).
process.stderr.write(JSON.stringify({ error: `--${name} must be a number, got "${raw}"`, code: "BAD_ARG" }) + "\n") // Whole numbers >= 1 only, matching the other portal CLIs.
const val = typeof raw === "string" ? Number(raw.trim()) : NaN
if (!Number.isInteger(val) || val < 1) {
process.stderr.write(
JSON.stringify({ error: `--${name} must be a whole number of at least 1, got "${raw}"`, code: "BAD_ARG" }) + "\n",
)
return null return null
} }
return val return val
@@ -140,15 +145,8 @@ async function main(): Promise<number> {
flags.jobage = String(v) flags.jobage = String(v)
} }
if (flags["jobage-minutes"] !== undefined) { if (flags["jobage-minutes"] !== undefined) {
const raw = flags["jobage-minutes"] const v = parseIntFlag("jobage-minutes", flags["jobage-minutes"])
const v = parseIntFlag("jobage-minutes", raw)
if (v === null) return 1 if (v === null) return 1
if (v <= 0) {
process.stderr.write(
JSON.stringify({ error: `--jobage-minutes must be a positive number, got "${raw}"`, code: "BAD_ARG" }) + "\n",
)
return 1
}
flags["jobage-minutes"] = String(v) flags["jobage-minutes"] = String(v)
} }
if (flags.page !== undefined) { if (flags.page !== undefined) {
@@ -12,7 +12,7 @@ function parsedStderr(stderr: string): { error?: string; code?: string } {
} }
describe("LinkedIn CLI flag validation", () => { describe("LinkedIn CLI flag validation", () => {
describe("--jobage NaN validation", () => { describe("numeric flag validation", () => {
test("non-numeric string exits 1 with BAD_ARG", async () => { test("non-numeric string exits 1 with BAD_ARG", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "foo"]); const result = await runCLI(["search", "-l", LOCATION, "--jobage", "foo"]);
expect(result.exitCode).not.toBe(0); expect(result.exitCode).not.toBe(0);
@@ -33,18 +33,34 @@ describe("LinkedIn CLI flag validation", () => {
expect(err.code).not.toBe("BAD_ARG"); expect(err.code).not.toBe("BAD_ARG");
}); });
test("float string truncated to integer, no error", async () => { // Fractional values must be rejected, not truncated: parseInt("0.5") is 0,
// parseInt("7.5") = 7, which is valid // and jobage 0 makes buildTimeFilter return null, so f_TPR is silently
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "7.5", "--limit", "1"]); // omitted from the outbound request while the CLI exits 0 (#371).
const err = parsedStderr(result.stderr); for (const name of ["jobage", "jobage-minutes", "page", "limit"]) {
expect(err.code).not.toBe("BAD_ARG"); test(`--${name} fractional exits 1 with BAD_ARG instead of truncating`, async () => {
const result = await runCLI(["search", "-l", LOCATION, `--${name}`, "1.5"]);
expect(result.exitCode).not.toBe(0);
const err = parsedStderr(result.stderr);
expect(err.code).toBe("BAD_ARG");
expect(err.error).toMatch(new RegExp(name));
});
}
test("--jobage 0.5 exits 1 with BAD_ARG instead of dropping the freshness filter", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "0.5"]);
expect(result.exitCode).not.toBe(0);
expect(parsedStderr(result.stderr).code).toBe("BAD_ARG");
}); });
test("zero is accepted (falsy int should not be treated as missing)", async () => { for (const name of ["jobage", "jobage-minutes", "page", "limit"]) {
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "0", "--limit", "1"]); test(`--${name} 0 exits 1 with BAD_ARG`, async () => {
const err = parsedStderr(result.stderr); const result = await runCLI(["search", "-l", LOCATION, `--${name}`, "0"]);
expect(err.code).not.toBe("BAD_ARG"); expect(result.exitCode).not.toBe(0);
}); const err = parsedStderr(result.stderr);
expect(err.code).toBe("BAD_ARG");
expect(err.error).toMatch(new RegExp(name));
});
}
}); });
describe("--jobage-minutes validation", () => { describe("--jobage-minutes validation", () => {
@@ -56,14 +72,6 @@ describe("LinkedIn CLI flag validation", () => {
expect(err.error).toMatch(/jobage-minutes/); expect(err.error).toMatch(/jobage-minutes/);
}); });
test("zero exits 1 with BAD_ARG", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage-minutes", "0"]);
expect(result.exitCode).not.toBe(0);
const err = parsedStderr(result.stderr);
expect(err.code).toBe("BAD_ARG");
expect(err.error).toMatch(/jobage-minutes/);
});
test("negative value is parsed as a missing value and exits 1 with BAD_ARG", async () => { test("negative value is parsed as a missing value and exits 1 with BAD_ARG", async () => {
// parseFlags in cli.ts treats a next-token starting with "-" as absent // parseFlags in cli.ts treats a next-token starting with "-" as absent
// (`next.startsWith("-")` → flag becomes boolean `true`), and there is no // (`next.startsWith("-")` → flag becomes boolean `true`), and there is no
+8
View File
@@ -41,6 +41,14 @@ per-file diff commands.
### Fixed ### Fixed
- **`linkedin-search` rejects fractional numeric flags instead of silently changing
the query** (#371) - bare `parseInt` truncated values before validation, so
`--jobage 0.5` became `0` and silently omitted LinkedIn's `f_TPR` freshness filter
while the CLI reported no argument error. `--jobage`, `--jobage-minutes`, `--page`,
and `--limit` now accept whole numbers >= 1 only and reject fractions and zero with
the stderr-JSON `BAD_ARG` contract, matching the other portal CLIs. Pinned by eight
cases verified to fail on the unfixed CLI. Reported by @Meet6338-X.
- **`linkedin-search detail` accepts LinkedIn job URLs with trailing slashes** (#411) - - **`linkedin-search detail` accepts LinkedIn job URLs with trailing slashes** (#411) -
passing a job URL with a trailing slash (e.g., `https://www.linkedin.com/jobs/view/<id>/` passing a job URL with a trailing slash (e.g., `https://www.linkedin.com/jobs/view/<id>/`
or a slugged variant with or without query strings) failed validation and exited 1 with or a slugged variant with or without query strings) failed validation and exited 1 with