fix(jobnet-search): route external ads to jobAdUrl with http(s) validation and fallback (#538) (#557)

External ads (isExternal: true) 404 on /find-job/{jobAdId} and hit the MitID login wall on /job/{jobAdId}. Route them to their employer-provided destination in jobAdUrl when an absolute http(s) URL is present.

When jobAdUrl is missing, empty, or carries an invalid/non-http scheme, fall back to https://jobnet.dk/find-job/{jobAdId} instead of emitting an empty string so downstream /scrape deduplication preserves unique URLs. Both url and jobAdUrl validate the scheme consistently.

Observation and test cases reported by NishilRathod in #557.
This commit is contained in:
Ahmed authored and GitHub committed 2026-10-15 18:15:21 +02:00
1 parent e2b966d0e7
commit 4a8e63ea7c
5 files changed
+117 -38

No files matched your search

@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
buildSearchParams,
createSearchOutput,
externalAdUrl,
type SearchApiResponse,
type SearchFlags,
} from "../src/commands/search";
@@ -71,6 +72,7 @@ function apiResponse(): SearchApiResponse {
applicationDeadlineStatus: "ExpirationDate",
workHourPartTime: false,
isExternal: true,
jobAdUrl: "https://example.com/jobs/job-2",
hasLogo: true,
logoUrl: "/logo/job-2",
cvr: "12345678",
@@ -146,7 +148,8 @@ describe("Jobnet search normalization", () => {
location: "København Ø",
date: "2026-07-02",
deadline: "2026-08-01",
url: "https://jobnet.dk/find-job/job-2",
url: "https://example.com/jobs/job-2",
jobAdUrl: "https://example.com/jobs/job-2",
});
expect(output.results[0].hiringOrgName).toBe("Acme");
expect(output.results[1].applicationDeadline).toBe("2026-08-01T23:59:00+02:00");
@@ -154,6 +157,33 @@ describe("Jobnet search normalization", () => {
expect(output.results[0].id).toBe(output.results[0].jobAdId);
});
test("external ads emit destination jobAdUrl as url, or fallback to find-job link when missing/invalid", () => {
const response = apiResponse();
// External ad with valid http(s) jobAdUrl emits that URL
response.jobAds[1].isExternal = true;
response.jobAds[1].jobAdUrl = "https://employer.example/apply";
const withUrl = createSearchOutput(response, { ...flags, limit: undefined });
expect(withUrl.results[1].url).toBe("https://employer.example/apply");
expect(withUrl.results[1].jobAdUrl).toBe("https://employer.example/apply");
// External ad without jobAdUrl falls back to find-job link to preserve a unique URL for /scrape deduplication
response.jobAds[1].jobAdUrl = null;
const withoutUrl = createSearchOutput(response, { ...flags, limit: undefined });
expect(withoutUrl.results[1].url).toBe("https://jobnet.dk/find-job/job-2");
expect(withoutUrl.results[1].jobAdUrl).toBeNull();
expect(withoutUrl.results[1].isExternal).toBe(true);
// External ad with non-http(s) scheme falls back to find-job link and clears jobAdUrl
response.jobAds[1].jobAdUrl = "javascript:alert(1)";
const invalidUrl = createSearchOutput(response, { ...flags, limit: undefined });
expect(invalidUrl.results[1].url).toBe("https://jobnet.dk/find-job/job-2");
expect(invalidUrl.results[1].jobAdUrl).toBeNull();
// Internal ad always keeps find-job link
expect(withoutUrl.results[0].url).toBe("https://jobnet.dk/find-job/job-1");
expect(withoutUrl.results[0].isExternal).toBe(false);
});
test("maps Jobnet's undisclosed-deadline sentinel (1900-01-01) to null", () => {
const response = apiResponse();
response.jobAds[0].applicationDeadline = "1900-01-01T00:00:00+01:00";
@@ -185,3 +215,19 @@ describe("Jobnet null publicationDate degradation", () => {
expect(output.results[1].date).toBe("2026-07-02");
});
});
describe("externalAdUrl helper", () => {
test("returns valid https or http URL when isExternal is true", () => {
expect(externalAdUrl({ isExternal: true, jobAdUrl: "https://example.com/apply" })).toBe("https://example.com/apply");
expect(externalAdUrl({ isExternal: true, jobAdUrl: "http://example.com/apply" })).toBe("http://example.com/apply");
});
test("returns null if not external, missing, or non-http(s) scheme", () => {
expect(externalAdUrl({ isExternal: false, jobAdUrl: "https://example.com/apply" })).toBeNull();
expect(externalAdUrl({ isExternal: true, jobAdUrl: null })).toBeNull();
expect(externalAdUrl({ isExternal: true, jobAdUrl: " " })).toBeNull();
expect(externalAdUrl({ isExternal: true, jobAdUrl: "javascript:void(0)" })).toBeNull();
expect(externalAdUrl({ isExternal: true, jobAdUrl: "file:///etc/passwd" })).toBeNull();
expect(externalAdUrl({ isExternal: true, jobAdUrl: "not-a-valid-url" })).toBeNull();
});
});