mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 16:46:24 +00:00
The guard in the four bunli-based CLIs inspected only tokens starting with `--`, so an undefined short flag bypassed it: bunli discarded it, the search ran unfiltered, and the CLI exited 0. Live against jobnet, `search -q "sygeplejerske"` returned all 18,179 ads as a successful search against 667 for the real `--search-string` query - the same shape as review finding F13 that motivated the guard. Both dash forms are now checked. Declared shorts (jobindex's -q) and bunli's built-in -h/-v stay valid. A negative number is rejected too: bunli discards a `-`-prefixed token rather than consuming it as the previous flag's value, so `--radius -5` silently fell back to the default instead of failing its own min(1) schema; a value that must begin with a dash uses the `--flag=value` form. Fixes #426. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
112 lines
5.1 KiB
TypeScript
112 lines
5.1 KiB
TypeScript
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. 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);
|
|
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("Jobbank CLI flag validation", () => {
|
|
test("search --limit=-1 is rejected instead of silently dropping the last result", async () => {
|
|
const result = await runCLI(["search", "--key", "test", "--limit=-1"]);
|
|
expectValidationError(result, "limit");
|
|
expect(JSON.parse(result.stderr).error.message).toContain("greater than or equal to 1");
|
|
});
|
|
|
|
test("search --limit=0 is rejected", async () => {
|
|
const result = await runCLI(["search", "--key", "test", "--limit=0"]);
|
|
expectValidationError(result, "limit");
|
|
});
|
|
|
|
test("search --limit=1.5 is rejected as non-integer", async () => {
|
|
const result = await runCLI(["search", "--key", "test", "--limit=1.5"]);
|
|
expectValidationError(result, "limit");
|
|
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"]);
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(JSON.parse(result.stderr)).toEqual({
|
|
error: "--key or at least one filter is required",
|
|
code: "MISSING_REQUIRED",
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe("unknown flag rejection", () => {
|
|
// add-portal.md's contract: "a bogus flag or missing required arg exits 1
|
|
// with a JSON error on stderr". A silently discarded flag is worse than an
|
|
// error: on jobdanmark a wrong flag name returned the entire database
|
|
// (13,862 results) as if it matched the query (review finding F13,
|
|
// 2026-08-19). Rejection happens before dispatch, so these are network-free.
|
|
test("a bogus --flag exits 1 with a JSON error instead of being silently discarded", async () => {
|
|
const result = await runCLI(["search", "--key", "test", "--bogus-flag", "xyz"]);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stdout).toBe("");
|
|
const error = JSON.parse(result.stderr);
|
|
expect(error.code).toBe("UNKNOWN_FLAG");
|
|
expect(error.error).toContain("--bogus-flag");
|
|
});
|
|
|
|
test("--query (another portal's free-text flag) is rejected, not treated as no filter", async () => {
|
|
const result = await runCLI(["search", "--query", "test"]);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(JSON.parse(result.stderr).code).toBe("UNKNOWN_FLAG");
|
|
});
|
|
// #426: the guard inspected only `--long` tokens, so a single-dash flag was
|
|
// discarded in silence - the same failure the long-form tests above pin,
|
|
// reached by the likelier route. `-q` is the documented short for the
|
|
// keyword search in linkedin-search, freehire-search and jobindex-search,
|
|
// so it is what a cross-portal habit produces here; live, it returned the
|
|
// portal's entire database as a successful, unfiltered search.
|
|
test("-q (another portal's short flag) is rejected, not treated as no filter", async () => {
|
|
const result = await runCLI(["search", "-q", "test"]);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stdout).toBe("");
|
|
const error = JSON.parse(result.stderr);
|
|
expect(error.code).toBe("UNKNOWN_FLAG");
|
|
expect(error.error).toContain("-q");
|
|
});
|
|
|
|
// bunli discards a `-`-prefixed token instead of consuming it as the
|
|
// previous flag's value, so a negative number never reached the option's
|
|
// own schema - it silently fell back to the default. Loud beats silent.
|
|
test("a negative number is rejected instead of silently falling back to the default", async () => {
|
|
const result = await runCLI(["search", "--key", "test", "--limit", "-5"]);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(JSON.parse(result.stderr).code).toBe("UNKNOWN_FLAG");
|
|
});
|
|
|
|
test("-h still prints help rather than being rejected as unknown", async () => {
|
|
const result = await runCLI(["search", "-h"]);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
});
|