mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
* fix(portal-clis): validate Jobbank and Jobnet detail URL hosts * fix(portal-clis): preserve scheme-less portal detail URLs Normalize host-like input with HTTPS before validating the portal host. Cover apex, www, mobile, uppercase and legacy routes while rejecting foreign or deceptive hosts and query-only posting paths. Thanks to MadsLorentzen for finding the compatibility regression in #672. * changelog: place the #672 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { normalizeJobId } from "../src/helpers.js"
|
|
import { runCLI } from "./helpers.js"
|
|
|
|
describe("jobbank-search normalizeJobId", () => {
|
|
test("accepts portal URLs without a scheme", () => {
|
|
for (const host of ["jobbank.dk", "www.jobbank.dk", "m.jobbank.dk", "JOBBANK.DK"]) {
|
|
expect(normalizeJobId(` ${host}/job/3117065/company/role?ref=search#apply `)).toBe("3117065")
|
|
}
|
|
})
|
|
|
|
test("rejects foreign hosts, non-web schemes and job paths only in a query", () => {
|
|
for (const value of [
|
|
"https://example.com/job/304212/",
|
|
"https://jobbank.dk.example.com/job/304212/",
|
|
"ftp://jobbank.dk/job/304212/",
|
|
"https://jobbank.dk/?next=/job/304212/",
|
|
"example.com/job/304212/",
|
|
"jobbank.dk.evil.io/job/304212/",
|
|
"jobbank.dk@evil.io/job/304212/",
|
|
"jobbank.dk/?next=/job/304212/",
|
|
]) expect(normalizeJobId(value)).toBeNull()
|
|
})
|
|
|
|
test("accepts the portal's www host and HTTP links", () => {
|
|
expect(normalizeJobId("http://www.jobbank.dk/job/304212/")).toBe("304212")
|
|
})
|
|
|
|
test("CLI rejects another site's matching job path before any fetch", async () => {
|
|
const proc = Bun.spawn([
|
|
process.execPath, "--preload", `${import.meta.dir}/reject-network.ts`,
|
|
`${import.meta.dir}/../src/cli.ts`, "detail", "https://example.com/job/304212/",
|
|
], { stdout: "pipe", stderr: "pipe" })
|
|
const [stdout, stderr, code] = await Promise.all([
|
|
new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited,
|
|
])
|
|
expect(code).toBe(1)
|
|
expect(stdout).toBe("")
|
|
expect(JSON.parse(stderr).code).toBe("BAD_ID")
|
|
})
|
|
|
|
test("accepts bare numeric ID", () => {
|
|
expect(normalizeJobId("304212")).toBe("304212")
|
|
expect(normalizeJobId(" 12345 ")).toBe("12345")
|
|
})
|
|
|
|
test("extracts ID from full URL with trailing slash", () => {
|
|
expect(normalizeJobId("https://jobbank.dk/job/304212/")).toBe("304212")
|
|
})
|
|
|
|
test("extracts ID from full URL without trailing slash", () => {
|
|
expect(normalizeJobId("https://jobbank.dk/job/304212")).toBe("304212")
|
|
})
|
|
|
|
test("extracts ID from full URL with company/role slug segments", () => {
|
|
expect(normalizeJobId("https://jobbank.dk/job/304212/acme-corp/software-developer")).toBe("304212")
|
|
expect(normalizeJobId("https://jobbank.dk/job/304212/acme-corp/software-developer/")).toBe("304212")
|
|
})
|
|
|
|
test("extracts ID from URL with query parameters and hash fragments", () => {
|
|
expect(normalizeJobId("https://jobbank.dk/job/304212?ref=search&page=1")).toBe("304212")
|
|
expect(normalizeJobId("https://jobbank.dk/job/304212#apply")).toBe("304212")
|
|
})
|
|
|
|
test("rejects invalid non-numeric strings and unrelated URLs", () => {
|
|
expect(normalizeJobId("abc")).toBeNull()
|
|
expect(normalizeJobId("https://example.com/other/12345")).toBeNull()
|
|
expect(normalizeJobId("")).toBeNull()
|
|
})
|
|
|
|
test("CLI detail command rejects invalid ID format with BAD_ID", async () => {
|
|
const result = await runCLI(["detail", "invalid-id-format"])
|
|
expect(result.exitCode).toBe(1)
|
|
const err = JSON.parse(result.stderr)
|
|
expect(err.code).toBe("BAD_ID")
|
|
})
|
|
})
|