106 lines
5.5 KiB
JavaScript
106 lines
5.5 KiB
JavaScript
// tests/providers/echojobs.test.mjs — provider-contract tests for the EchoJobs
|
|
// board-wide JSON aggregator (providers/echojobs.mjs).
|
|
import { pass, fail, ROOT } from '../helpers.mjs';
|
|
import { join } from 'path';
|
|
import { pathToFileURL } from 'url';
|
|
|
|
console.log('\nProvider — echojobs');
|
|
|
|
try {
|
|
const echojobsModule = await import(pathToFileURL(join(ROOT, 'providers/echojobs.mjs')).href);
|
|
const echojobs = echojobsModule.default;
|
|
const { normalizeEchojobsJob } = echojobsModule;
|
|
|
|
if (echojobs.id === 'echojobs') pass('echojobs.id is "echojobs"');
|
|
else fail(`echojobs.id is ${JSON.stringify(echojobs.id)}`);
|
|
|
|
// detect() — explicit provider selection only (board-wide feed)
|
|
const hit = echojobs.detect({ name: 'EchoJobs', provider: 'echojobs' });
|
|
if (hit && hit.url === 'https://echojobs.io/api/jobs') pass('echojobs.detect() resolves provider:echojobs → feed URL');
|
|
else fail(`echojobs.detect() returned ${JSON.stringify(hit)}`);
|
|
if (echojobs.detect({ name: 'X' }) === null) pass('echojobs.detect() returns null without provider:echojobs');
|
|
else fail('echojobs.detect() should require provider:echojobs');
|
|
|
|
// normalizeEchojobsJob — field mapping, external ATS url kept, postedAt (ms)
|
|
const j = {
|
|
title: ' Senior Go Engineer ',
|
|
url: 'https://jobs.ashbyhq.com/acme/abc-123',
|
|
company_name: 'Acme',
|
|
locations: ['San Francisco, CA', 'New York, NY'],
|
|
remote_type: 'on_site',
|
|
posted_at: 1783380913765,
|
|
};
|
|
const n = normalizeEchojobsJob(j, 'Fallback');
|
|
if (n && n.title === 'Senior Go Engineer' && n.company === 'Acme' &&
|
|
n.url === 'https://jobs.ashbyhq.com/acme/abc-123' && n.location === 'San Francisco, CA, New York, NY' &&
|
|
n.postedAt === 1783380913765) {
|
|
pass('normalizeEchojobsJob maps title/company/url/location/postedAt and keeps the external ATS URL');
|
|
} else {
|
|
fail(`normalizeEchojobsJob => ${JSON.stringify(n)}`);
|
|
}
|
|
|
|
// remote fallback when no listed place
|
|
const remote = normalizeEchojobsJob({ title: 'X', url: 'https://jobs.lever.co/x/1', locations: [], remote_type: 'remote' });
|
|
const hybrid = normalizeEchojobsJob({ title: 'X', url: 'https://jobs.lever.co/x/2', remote_type: 'hybrid' });
|
|
if (remote?.location === 'Remote' && hybrid?.location === 'Remote') {
|
|
pass('normalizeEchojobsJob falls back to "Remote" for a placeless remote OR hybrid role');
|
|
} else {
|
|
fail(`remote/hybrid fallback => ${JSON.stringify([remote?.location, hybrid?.location])}`);
|
|
}
|
|
|
|
// company fallback to the entry name
|
|
const bare = normalizeEchojobsJob({ title: 'X', url: 'https://jobs.lever.co/x/1' }, 'EntryName');
|
|
if (bare?.company === 'EntryName') pass('normalizeEchojobsJob falls back to the entry name for company');
|
|
else fail(`company fallback => ${JSON.stringify(bare?.company)}`);
|
|
|
|
// drops: no title, non-https url, missing url
|
|
if (normalizeEchojobsJob({ url: 'https://x/1' }) === null) pass('normalizeEchojobsJob drops a title-less item');
|
|
else fail('title-less item should be dropped');
|
|
if (normalizeEchojobsJob({ title: 'X', url: 'http://insecure/1' }) === null) pass('normalizeEchojobsJob drops a non-https url');
|
|
else fail('non-https url should be dropped');
|
|
if (normalizeEchojobsJob({ title: 'X' }) === null) pass('normalizeEchojobsJob drops an item with no url');
|
|
else fail('url-less item should be dropped');
|
|
|
|
// fetch() — pagination: accumulates across pages, stops on a short page, and
|
|
// pins the feed request to echojobs.io with redirect:'error'.
|
|
const page1 = { jobs: Array.from({ length: 100 }, (_, i) => ({ title: `Job ${i}`, url: `https://jobs.ashbyhq.com/acme/${i}`, company_name: 'Acme' })) };
|
|
const page2 = { jobs: [{ title: 'Last', url: 'https://jobs.ashbyhq.com/acme/last', company_name: 'Acme' }] };
|
|
const calls = [];
|
|
const ctx = {
|
|
transport: 'http',
|
|
fetchJson: async (url, options) => {
|
|
calls.push(url);
|
|
if (options?.redirect !== 'error') throw new Error(`fetchJson without redirect:'error': ${JSON.stringify(options)}`);
|
|
if (new URL(url).hostname !== 'echojobs.io') throw new Error(`fetchJson off-host: ${url}`);
|
|
return new URL(url).searchParams.get('page') === '1' ? page1 : page2;
|
|
},
|
|
fetchText: async () => { throw new Error('fetchText should not be called'); },
|
|
};
|
|
const jobs = await echojobs.fetch({ name: 'EchoJobs', provider: 'echojobs' }, ctx);
|
|
if (jobs.length === 101) pass('echojobs.fetch() accumulates across pages and stops on the short page (100 + 1)');
|
|
else fail(`echojobs.fetch() returned ${jobs.length} jobs, expected 101`);
|
|
if (calls.length === 2) pass('echojobs.fetch() stopped after the short second page (2 requests)');
|
|
else fail(`echojobs.fetch() made ${calls.length} page requests, expected 2`);
|
|
|
|
// max_pages override caps pagination
|
|
const cappedCalls = [];
|
|
await echojobs.fetch(
|
|
{ name: 'EchoJobs', provider: 'echojobs', max_pages: 1 },
|
|
{ transport: 'http', fetchJson: async (u) => { cappedCalls.push(u); return page1; }, fetchText: async () => {} },
|
|
);
|
|
if (cappedCalls.length === 1) pass('echojobs.fetch() respects max_pages (stops after 1 page)');
|
|
else fail(`echojobs.fetch() max_pages=1 made ${cappedCalls.length} page calls`);
|
|
|
|
// unexpected shape throws
|
|
let threw = false;
|
|
try {
|
|
await echojobs.fetch({ name: 'EchoJobs', provider: 'echojobs' },
|
|
{ transport: 'http', fetchJson: async () => ({ oops: true }), fetchText: async () => {} });
|
|
} catch { threw = true; }
|
|
if (threw) pass('echojobs.fetch() throws on an unexpected API response shape');
|
|
else fail('echojobs.fetch() should throw when the response has no jobs array');
|
|
|
|
} catch (e) {
|
|
fail(`echojobs provider tests crashed: ${e.message}`);
|
|
}
|