mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-08 16:07:40 +00:00
fix(freehire-search): reject detail URLs on other hosts instead of asking the API for their last segment (#622) (#623)
* fix(freehire-search): reject detail URLs on other hosts instead of asking the API for their last segment normalizeSlug took the segment after "/jobs/" from ANY string, so a Greenhouse apply link - which is what a freehire search result's own url field carries - was reduced to its last path segment and detail requested /api/v1/jobs/1, then reported NOT_FOUND for a URL that was never a freehire posting. Every other portal CLI rejects an off-host detail URL with BAD_ID (#447, #430, #499); freehire was the last one trusting the path. Parse URLs for real: freehire.me (apex or any subdomain, scheme optional) or the host of FREEHIRE_API_URL for a self-hosted instance, plus a /jobs/<slug> path, yields the slug; other hosts, look-alike and userinfo hosts, and non-job freehire pages exit 1 with BAD_ID before any request. Bare slugs are unchanged. Five new normalizeSlug cases and a handler-level test that pastes the search fixture's own apply URL into detail with fetch stubbed; the off-host cases fail on master. * changelog: place the #623 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>
This commit is contained in:
5 files changed
+112
-4
No files matched your search
@@ -235,3 +235,20 @@ describe("runDetail (mocked fetch)", () => {
|
||||
expect(JSON.parse(err).code).toBe("NOT_FOUND");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detail input on the real handler path", () => {
|
||||
test("a search result's own apply URL is rejected with BAD_ID before any request", async () => {
|
||||
// Every freehire search result carries the employer's apply link in `url`
|
||||
// (the fixture's is a Greenhouse board). Pasting it back into detail used
|
||||
// to request /api/v1/jobs/1 and report NOT_FOUND for a URL that was never
|
||||
// a freehire posting.
|
||||
const mock = mockFetch(404, { error: "not found" });
|
||||
const err = captureStderr();
|
||||
|
||||
const code = await runDetail({ id: job().url, format: "json" });
|
||||
|
||||
expect(code).toBe(1);
|
||||
expect(mock.url()).toBe("");
|
||||
expect(JSON.parse(err.get())).toMatchObject({ code: "BAD_ID" });
|
||||
});
|
||||
});
|
||||
@@ -147,4 +147,44 @@ describe("normalizeSlug", () => {
|
||||
expect(normalizeSlug("not a slug!")).toBeNull();
|
||||
expect(normalizeSlug("")).toBeNull();
|
||||
});
|
||||
test("accepts subdomains, a scheme-less host, a trailing slash and a query string", () => {
|
||||
expect(normalizeSlug("https://www.freehire.me/jobs/golang-zensar-2bxu6dxm/")).toBe("golang-zensar-2bxu6dxm");
|
||||
expect(normalizeSlug("freehire.me/jobs/golang-zensar-2bxu6dxm?utm_source=x")).toBe("golang-zensar-2bxu6dxm");
|
||||
});
|
||||
test("rejects a /jobs/ URL on another host instead of asking the API for its last segment", () => {
|
||||
// The search fixture's own `url` is a Greenhouse apply link; pasting it
|
||||
// back into detail used to request /api/v1/jobs/1 and report NOT_FOUND.
|
||||
expect(normalizeSlug("https://boards.greenhouse.io/acme/jobs/1")).toBeNull();
|
||||
expect(normalizeSlug("https://jobs.lever.co/acme/jobs/abc")).toBeNull();
|
||||
});
|
||||
test("rejects look-alike and userinfo hosts", () => {
|
||||
expect(normalizeSlug("https://freehire.me.evil.io/jobs/x")).toBeNull();
|
||||
expect(normalizeSlug("https://notfreehire.me/jobs/x")).toBeNull();
|
||||
expect(normalizeSlug("https://freehire.me@evil.io/jobs/x")).toBeNull();
|
||||
});
|
||||
test("rejects a freehire.me URL that is not a job page", () => {
|
||||
expect(normalizeSlug("https://freehire.me/companies/acme")).toBeNull();
|
||||
expect(normalizeSlug("https://freehire.me/jobs/")).toBeNull();
|
||||
});
|
||||
test("requires the path to be exactly /jobs/<slug>", () => {
|
||||
// The slug is the whole path after /jobs/: a nested path, a prefix before
|
||||
// /jobs/, or a slug with characters outside the slug alphabet is not a
|
||||
// freehire job page, even on the right host.
|
||||
expect(normalizeSlug("https://freehire.me/jobs/my-slug/apply")).toBeNull();
|
||||
expect(normalizeSlug("https://freehire.me/en/jobs/my-slug")).toBeNull();
|
||||
expect(normalizeSlug("https://freehire.me/jobs/my_slug")).toBeNull();
|
||||
expect(normalizeSlug("https://freehire.me/jobs/my-slug#details")).toBe("my-slug");
|
||||
});
|
||||
|
||||
test("accepts the self-hosted base URL's host", () => {
|
||||
const saved = process.env.FREEHIRE_API_URL;
|
||||
process.env.FREEHIRE_API_URL = "http://localhost:8080";
|
||||
try {
|
||||
expect(normalizeSlug("http://localhost:8080/jobs/my-slug")).toBe("my-slug");
|
||||
expect(normalizeSlug("https://boards.greenhouse.io/acme/jobs/1")).toBeNull();
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env.FREEHIRE_API_URL;
|
||||
else process.env.FREEHIRE_API_URL = saved;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user