162 lines
8.4 KiB
JavaScript
162 lines
8.4 KiB
JavaScript
// tests/providers/4dayweek.test.mjs — renamed from fourdayweek.test.mjs to match
|
|
// the provider id, so `--only providers/4dayweek` discovers it (#1657).
|
|
import { pass, fail, ROOT } from '../helpers.mjs';
|
|
import { join } from 'path';
|
|
import { pathToFileURL } from 'url';
|
|
|
|
console.log('\nProvider — 4dayweek');
|
|
|
|
try {
|
|
const fdwModule = await import(pathToFileURL(join(ROOT, 'providers/4dayweek.mjs')).href);
|
|
const { resolveProvider } = await import(pathToFileURL(join(ROOT, 'providers/_registry.mjs')).href);
|
|
const fourdayweek = fdwModule.default;
|
|
const { normalize4dwJob } = fdwModule;
|
|
|
|
if (fourdayweek.id === '4dayweek') pass('4dayweek.id is "4dayweek"');
|
|
else fail(`4dayweek.id is ${JSON.stringify(fourdayweek.id)}`);
|
|
|
|
const explicitHit = fourdayweek.detect({ name: '4 Day Week', provider: '4dayweek' });
|
|
if (explicitHit?.url === 'https://4dayweek.io/api/jobs') {
|
|
pass('4dayweek.detect() claims explicit provider config');
|
|
} else {
|
|
fail(`4dayweek.detect() explicit provider = ${JSON.stringify(explicitHit)}`);
|
|
}
|
|
|
|
const careersHit = fourdayweek.detect({ name: '4 Day Week', careers_url: 'https://4dayweek.io/jobs' });
|
|
const apiHit = fourdayweek.detect({ name: '4 Day Week', api: 'https://4dayweek.io/api/jobs' });
|
|
if (careersHit?.url === 'https://4dayweek.io/api/jobs' && apiHit?.url === 'https://4dayweek.io/api/jobs') {
|
|
pass('4dayweek.detect() maps trusted 4dayweek.io URLs to the feed URL');
|
|
} else {
|
|
fail(`4dayweek.detect() trusted URLs = ${JSON.stringify({ careersHit, apiHit })}`);
|
|
}
|
|
|
|
const detectMisses = [
|
|
fourdayweek.detect({ name: 'X' }),
|
|
fourdayweek.detect({ name: 'Other', provider: 'echojobs', careers_url: 'https://4dayweek.io/jobs' }),
|
|
fourdayweek.detect({ name: 'Path spoof', careers_url: 'https://evil.example/4dayweek.io/jobs' }),
|
|
fourdayweek.detect({ name: 'Suffix spoof', careers_url: 'https://4dayweek.io.evil.example/jobs' }),
|
|
fourdayweek.detect({ name: 'HTTP', careers_url: 'http://4dayweek.io/jobs' }),
|
|
fourdayweek.detect({ name: 'Non-string', careers_url: 42 }),
|
|
];
|
|
if (detectMisses.every(hit => hit === null)) {
|
|
pass('4dayweek.detect() rejects missing, other-provider, spoofed, http, and non-string URLs');
|
|
} else {
|
|
fail(`4dayweek.detect() misses = ${JSON.stringify(detectMisses)}`);
|
|
}
|
|
|
|
const resolved = resolveProvider(
|
|
{ name: '4 Day Week', careers_url: 'https://4dayweek.io/jobs' },
|
|
new Map([['4dayweek', fourdayweek]]),
|
|
);
|
|
if (resolved && 'provider' in resolved && resolved.provider === fourdayweek) {
|
|
pass('provider registry dispatches 4dayweek.detect() for trusted 4dayweek.io URLs');
|
|
} else {
|
|
fail(`provider registry did not dispatch 4dayweek.detect(): ${JSON.stringify(resolved)}`);
|
|
}
|
|
|
|
// normalize4dwJob — full mapping (url is BUILT from slug; feed has no url).
|
|
const full = normalize4dwJob(
|
|
{ title: ' Financial Controller ', slug: 'financial-controller-at-panzerglass-45369c18', company_name: ' PanzerGlass ', locations: [{ city: 'Hinnerup', country: 'Denmark' }], work_arrangement: 'onsite', posted: 1782731975, is_expired: false },
|
|
'Fallback',
|
|
);
|
|
if (full && full.title === 'Financial Controller'
|
|
&& full.url === 'https://4dayweek.io/job/financial-controller-at-panzerglass-45369c18'
|
|
&& full.company === 'PanzerGlass' && full.location === 'Hinnerup, Denmark'
|
|
&& full.postedAt === 1782731975 * 1000) {
|
|
pass('normalize4dwJob maps title, builds /job/<slug> url, company_name, location, posted(seconds)→ms');
|
|
} else {
|
|
fail(`normalize4dwJob full row = ${JSON.stringify(full)}`);
|
|
}
|
|
|
|
// work_arrangement: remote → "Remote" appended.
|
|
const remoteJob = normalize4dwJob({ title: 'R', slug: 'r-1', locations: [{ city: 'Berlin', country: 'Germany' }], work_arrangement: 'remote' });
|
|
if (remoteJob?.location === 'Berlin, Germany, Remote') pass('normalize4dwJob appends "Remote" when work_arrangement is "remote"');
|
|
else fail(`normalize4dwJob remote location = ${JSON.stringify(remoteJob?.location)}`);
|
|
|
|
// company fallbacks: company.name → entry name → "4 Day Week" (whitespace-only ignored).
|
|
const coNested = normalize4dwJob({ title: 'T', slug: 's-1', company: { name: 'Nested Co' } });
|
|
const coEntry = normalize4dwJob({ title: 'T', slug: 's-2' }, 'Entry Name');
|
|
const coDefault = normalize4dwJob({ title: 'T', slug: 's-3' });
|
|
const coBlank = normalize4dwJob({ title: 'T', slug: 's-4' }, ' ');
|
|
if (coNested?.company === 'Nested Co' && coEntry?.company === 'Entry Name'
|
|
&& coDefault?.company === '4 Day Week' && coBlank?.company === '4 Day Week') {
|
|
pass('normalize4dwJob falls back company → company.name → entry name → "4 Day Week" (whitespace-only ignored)');
|
|
} else {
|
|
fail(`normalize4dwJob company fallbacks = ${JSON.stringify({ n: coNested?.company, e: coEntry?.company, d: coDefault?.company, b: coBlank?.company })}`);
|
|
}
|
|
|
|
// postedAt omitted when posted is absent / non-finite.
|
|
const noDate = normalize4dwJob({ title: 'T', slug: 's-5' });
|
|
const nanDate = normalize4dwJob({ title: 'T', slug: 's-6', posted: 'oops' });
|
|
if (noDate && !('postedAt' in noDate) && nanDate && !('postedAt' in nanDate)) {
|
|
pass('normalize4dwJob omits postedAt when posted is absent or non-numeric (NaN-safe)');
|
|
} else {
|
|
fail(`normalize4dwJob date handling = ${JSON.stringify({ none: noDate, nan: nanDate })}`);
|
|
}
|
|
|
|
// drops: expired, empty title, missing/unsafe slug, non-object.
|
|
const drops = [
|
|
normalize4dwJob({ title: 'Expired', slug: 'x-1', is_expired: true }),
|
|
normalize4dwJob({ title: '', slug: 'x-2' }),
|
|
normalize4dwJob({ title: 'No slug' }),
|
|
normalize4dwJob({ title: 'Unsafe slug', slug: 'a/b' }),
|
|
normalize4dwJob({ title: 'Spacey slug', slug: 'a b' }),
|
|
normalize4dwJob(null),
|
|
];
|
|
if (drops.every(r => r === null)) {
|
|
pass('normalize4dwJob drops expired / empty-title / no-slug / unsafe-slug / non-object');
|
|
} else {
|
|
fail(`normalize4dwJob drops = ${JSON.stringify(drops)}`);
|
|
}
|
|
|
|
// fetch(): pagination by ?page=N, stop on has_more:false.
|
|
const mk = (i) => ({ title: `Role ${i}`, slug: `role-${i}`, company_name: `Co ${i}`, locations: [{ city: 'Lisbon', country: 'Portugal' }], posted: 1782731975 + i, is_expired: false });
|
|
const page1 = { jobs: Array.from({ length: 25 }, (_, i) => mk(i)), total: 50, page: 1, has_more: true };
|
|
const page2 = { jobs: [mk(25), mk(26), { title: '', slug: 'bad' }], total: 50, page: 2, has_more: false }; // has_more:false → stop; 1 drop
|
|
const requested = [];
|
|
const pagedFetch = async (url, opts) => {
|
|
requested.push({ url, redirect: opts?.redirect });
|
|
return Number(new URL(url).searchParams.get('page')) === 1 ? page1 : page2;
|
|
};
|
|
const paged = await fourdayweek.fetch({ name: '4 Day Week' }, { fetchJson: pagedFetch });
|
|
|
|
if (requested.length === 2
|
|
&& requested[0].url === 'https://4dayweek.io/api/jobs?page=1'
|
|
&& requested[1].url === 'https://4dayweek.io/api/jobs?page=2') {
|
|
pass('4dayweek.fetch() builds ?page=N URLs and stops when has_more is false');
|
|
} else {
|
|
fail(`4dayweek.fetch() requested = ${JSON.stringify(requested.map(r => r.url))}`);
|
|
}
|
|
|
|
if (requested.every(r => r.redirect === 'error')) pass('4dayweek.fetch() passes redirect:"error" on every page (SSRF guard)');
|
|
else fail(`4dayweek.fetch() redirect opts = ${JSON.stringify(requested.map(r => r.redirect))}`);
|
|
|
|
if (paged.length === 27) pass('4dayweek.fetch() aggregates valid jobs across pages (25 + 2, dropping the empty-title row)');
|
|
else fail(`4dayweek.fetch() returned ${paged.length} jobs (expected 27)`);
|
|
|
|
// max_pages cap: only the first page is requested even though has_more is true.
|
|
const capReq = [];
|
|
await fourdayweek.fetch(
|
|
{ name: '4 Day Week', max_pages: 1 },
|
|
{ fetchJson: async (url) => { capReq.push(url); return { jobs: Array.from({ length: 25 }, (_, i) => mk(i)), total: 999, has_more: true }; } },
|
|
);
|
|
if (capReq.length === 1 && capReq[0] === 'https://4dayweek.io/api/jobs?page=1') {
|
|
pass('4dayweek.fetch() honors max_pages (stops at the cap even when has_more is true)');
|
|
} else {
|
|
fail(`4dayweek.fetch() max_pages:1 requested ${JSON.stringify(capReq)}`);
|
|
}
|
|
|
|
// unexpected API response → throws.
|
|
let badThrew = false;
|
|
try {
|
|
await fourdayweek.fetch({ name: 'X' }, { fetchJson: async () => ([]) });
|
|
} catch (e) {
|
|
badThrew = /unexpected API response/.test(e.message);
|
|
}
|
|
if (badThrew) pass('4dayweek.fetch() throws on unexpected API response shape (no jobs array)');
|
|
else fail('4dayweek.fetch() should throw when the jobs array is absent');
|
|
|
|
} catch (e) {
|
|
fail(`4dayweek provider tests crashed: ${e.message}`);
|
|
}
|