test(cli): cover Jobbank and Jobdanmark contracts (#148)

Jobbank and Jobdanmark each had only one narrow test, leaving required-argument errors, RSS normalization, JSON-LD variants, and malformed-page handling unprotected.

Add network-free fixture and subprocess tests for repeated RSS filters, description and ID parsing, stderr JSON errors, Bunli numeric validation, JSON-LD objects and arrays, optional fields, not-found pages, and parse failures.

The suites now cover eight Jobbank cases and six Jobdanmark cases without making live portal requests.
This commit is contained in:
Ayobami Adegoke
2026-07-13 20:45:38 +02:00
committed by GitHub
parent fca1c4234e
commit 03798ec974
4 changed files with 242 additions and 0 deletions
@@ -0,0 +1,30 @@
import { describe, expect, test } from "bun:test";
import { runCLI } from "./helpers";
function parseError(stderr: string): { error: string; code: string } {
return JSON.parse(stderr);
}
describe("Jobbank CLI error contract", () => {
test("search without a filter fails with JSON on stderr", async () => {
const result = await runCLI(["search"]);
expect(result.exitCode).toBe(1);
expect(result.stdout).toBe("");
expect(parseError(result.stderr)).toEqual({
error: "--key or at least one filter is required",
code: "MISSING_REQUIRED",
});
});
test("detail without an ID fails before making a request", async () => {
const result = await runCLI(["detail"]);
expect(result.exitCode).toBe(1);
expect(result.stdout).toBe("");
expect(parseError(result.stderr)).toEqual({
error: "Job ID is required",
code: "MISSING_REQUIRED",
});
});
});
@@ -0,0 +1,89 @@
import { afterEach, describe, expect, test } from "bun:test";
import {
extractJobIdFromUrl,
parseRssDescription,
rssFetch,
USER_AGENT,
} from "../src/helpers";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe("rssFetch", () => {
test("serializes repeated filters and parses CDATA and plain RSS fields", async () => {
let requestedUrl = "";
let requestedUserAgent = "";
globalThis.fetch = (async (input, init) => {
requestedUrl = String(input);
requestedUserAgent = new Headers(init?.headers).get("User-Agent") ?? "";
return new Response(`<?xml version="1.0"?>
<rss><channel><item>
<title><![CDATA[Data Scientist]]></title>
<description><![CDATA[Fuldtidsjob hos Acme A/S, København (Ansøgningsfrist: 31.07.2026)]]></description>
<link>https://jobbank.dk/job/12345/acme/data-scientist</link>
<pubDate>Mon, 13 Jul 2026 08:00:00 GMT</pubDate>
</item></channel></rss>`);
}) as typeof fetch;
const items = await rssFetch({ key: "data science", cvtype: ["3", "6"] });
const url = new URL(requestedUrl);
expect(url.pathname).toBe("/job/rss");
expect(url.searchParams.get("key")).toBe("data science");
expect(url.searchParams.getAll("cvtype")).toEqual(["3", "6"]);
expect(requestedUserAgent).toBe(USER_AGENT);
expect(items).toEqual([
{
title: "Data Scientist",
description: "Fuldtidsjob hos Acme A/S, København (Ansøgningsfrist: 31.07.2026)",
link: "https://jobbank.dk/job/12345/acme/data-scientist",
pubDate: "Mon, 13 Jul 2026 08:00:00 GMT",
},
]);
});
});
describe("parseRssDescription", () => {
test("separates multiple job types, company, location, and deadline", () => {
expect(
parseRssDescription(
"Fuldtidsjob, Graduate/trainee hos Acme A/S, København (Ansøgningsfrist: 31.07.2026)",
),
).toEqual({
jobType: "Fuldtidsjob, Graduate/trainee",
company: "Acme A/S",
location: "København",
deadline: "31.07.2026",
});
});
test("normalizes a rolling deadline to null", () => {
expect(
parseRssDescription("Deltidsjob hos Example ApS, Aarhus (Ansøgningsfrist: løbende)"),
).toEqual({
jobType: "Deltidsjob",
company: "Example ApS",
location: "Aarhus",
deadline: null,
});
});
test("keeps an unstructured description as the company fallback", () => {
expect(parseRssDescription("Unstructured feed value")).toEqual({
jobType: "",
company: "Unstructured feed value",
location: "",
deadline: null,
});
});
});
describe("extractJobIdFromUrl", () => {
test("extracts numeric IDs and rejects unsupported URL shapes", () => {
expect(extractJobIdFromUrl("https://jobbank.dk/job/12345/acme/role")).toBe("12345");
expect(extractJobIdFromUrl("https://jobbank.dk/job/not-a-number/acme/role")).toBe("");
});
});