mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-05 22:48:04 +00:00
fix(freehire-search): reject empty facet filters before searching (#561)
* fix(freehire-search): reject empty facet filters before searching * docs(changelog): record empty Freehire facet validation * docs(changelog): avoid overlap with upstream additions
This commit is contained in:
1 parent
4a8e63ea7c
commit
407dc34a2c
3 files changed
+67
-1
No files matched your search
@@ -39,7 +39,9 @@ function parseFlags(argv: string[]): Flags {
|
|||||||
// --facet repeats; collect into an array. Everything else is last-wins.
|
// --facet repeats; collect into an array. Everything else is last-wins.
|
||||||
if (key === "facet") {
|
if (key === "facet") {
|
||||||
const acc = Array.isArray(flags.facet) ? flags.facet : []
|
const acc = Array.isArray(flags.facet) ? flags.facet : []
|
||||||
if (typeof value === "string") acc.push(value)
|
// Preserve a bare occurrence so validation can reject it instead of
|
||||||
|
// silently dropping the requested filter, even after a valid --facet.
|
||||||
|
acc.push(typeof value === "string" ? value : "")
|
||||||
flags.facet = acc
|
flags.facet = acc
|
||||||
} else {
|
} else {
|
||||||
flags[key] = value
|
flags[key] = value
|
||||||
@@ -207,6 +209,10 @@ async function main(): Promise<number> {
|
|||||||
}
|
}
|
||||||
const key = kv.slice(0, eq)
|
const key = kv.slice(0, eq)
|
||||||
const vals = commaList(kv.slice(eq + 1))
|
const vals = commaList(kv.slice(eq + 1))
|
||||||
|
if (vals.length === 0) {
|
||||||
|
process.stderr.write(JSON.stringify({ error: `invalid --facet "${kv}", want key=value with at least one value`, code: "BAD_ARG" }) + "\n")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
facets[key] = (facets[key] ?? []).concat(vals)
|
facets[key] = (facets[key] ?? []).concat(vals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,61 @@ describe("freehire CLI flag validation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("--facet validation", () => {
|
describe("--facet validation", () => {
|
||||||
|
for (const args of [
|
||||||
|
["--facet"],
|
||||||
|
["--facet", "--limit", "1"],
|
||||||
|
["--facet", "salary_min="],
|
||||||
|
["--facet", "salary_min= , "],
|
||||||
|
["--facet", "countries=US", "--facet"],
|
||||||
|
]) {
|
||||||
|
test(`${args.join(" ")} rejects an empty filter before requesting jobs`, async () => {
|
||||||
|
const requests: string[] = [];
|
||||||
|
const server = Bun.serve({
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: 0,
|
||||||
|
fetch(request) {
|
||||||
|
requests.push(request.url);
|
||||||
|
return Response.json({ data: [], meta: { total: 0 } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCLI(["search", ...args], { FREEHIRE_API_URL: server.url.toString() });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.stdout).toBe("");
|
||||||
|
expect(parsedStderr(result.stderr).code).toBe("BAD_ARG");
|
||||||
|
expect(parsedStderr(result.stderr).error).toContain("--facet");
|
||||||
|
expect(requests).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("repeated facets and comma-separated values reach the API", async () => {
|
||||||
|
const requests: URL[] = [];
|
||||||
|
const server = Bun.serve({
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: 0,
|
||||||
|
fetch(request) {
|
||||||
|
requests.push(new URL(request.url));
|
||||||
|
return Response.json({ data: [], meta: { total: 0 } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await runCLI([
|
||||||
|
"search", "--facet", "countries=US, CA", "--facet", "countries=GB",
|
||||||
|
"--facet", "salary_min=100000", "--remote",
|
||||||
|
], { FREEHIRE_API_URL: server.url.toString() });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(requests).toHaveLength(1);
|
||||||
|
expect(requests[0].searchParams.getAll("countries")).toEqual(["US", "CA", "GB"]);
|
||||||
|
expect(requests[0].searchParams.get("salary_min")).toBe("100000");
|
||||||
|
expect(requests[0].searchParams.get("work_mode")).toBe("remote");
|
||||||
|
} finally {
|
||||||
|
server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("a facet without '=' exits 1 with BAD_ARG", async () => {
|
test("a facet without '=' exits 1 with BAD_ARG", async () => {
|
||||||
const result = await runCLI(["search", "--facet", "novalue"]);
|
const result = await runCLI(["search", "--facet", "novalue"]);
|
||||||
expect(result.exitCode).not.toBe(0);
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ per-file diff commands.
|
|||||||
derive both checks from the shipped files, so a renamed heading fails until `/reset`
|
derive both checks from the shipped files, so a renamed heading fails until `/reset`
|
||||||
follows it; both fail on master.
|
follows it; both fail on master.
|
||||||
|
|
||||||
|
- **`freehire-search` rejects empty `--facet` filters before requesting jobs** (#561) -
|
||||||
|
a bare flag (including one after a valid repeated facet), `salary_min=`, or a
|
||||||
|
comma-separated value list containing only whitespace now returns `BAD_ARG` and
|
||||||
|
exits 1 instead of silently dropping the filter and running a broader search.
|
||||||
|
|
||||||
- **The `/apply` host-check test derives its installed-portal hosts from the skills tree
|
- **The `/apply` host-check test derives its installed-portal hosts from the skills tree
|
||||||
instead of a hand-copied set** (#497, `tests/test_apply_host_check.py`) - `SHIPPED_PORTAL_HOSTS`
|
instead of a hand-copied set** (#497, `tests/test_apply_host_check.py`) - `SHIPPED_PORTAL_HOSTS`
|
||||||
was six names typed into the test and nothing tied it to anything: a portal added by
|
was six names typed into the test and nothing tied it to anything: a portal added by
|
||||||
|
|||||||
Reference in new issue
Block a user