208 lines
8.6 KiB
JavaScript
208 lines
8.6 KiB
JavaScript
// tests/providers/smartrecruiters.test.mjs — moved verbatim from test-all.mjs (#1440).
|
|
import { pass, fail, ROOT } from '../helpers.mjs';
|
|
import { join } from 'path';
|
|
import { pathToFileURL } from 'url';
|
|
|
|
console.log('\nProvider — smartrecruiters');
|
|
|
|
try {
|
|
const smartrecruitersModule = await import(pathToFileURL(join(ROOT, 'providers/smartrecruiters.mjs')).href);
|
|
const sr = smartrecruitersModule.default;
|
|
const { parseSmartRecruitersResponse } = smartrecruitersModule;
|
|
|
|
if (sr.id === 'smartrecruiters') pass('smartrecruiters.id is "smartrecruiters"');
|
|
else fail(`smartrecruiters.id is ${JSON.stringify(sr.id)}`);
|
|
|
|
const hitCareers = sr.detect({ name: 'Adyen', careers_url: 'https://careers.smartrecruiters.com/adyen' });
|
|
if (hitCareers && hitCareers.url.startsWith('https://api.smartrecruiters.com/v1/companies/adyen/postings')) {
|
|
pass('smartrecruiters.detect() resolves careers.smartrecruiters.com/<slug> → api URL');
|
|
} else {
|
|
fail(`smartrecruiters.detect(careers) returned ${JSON.stringify(hitCareers)}`);
|
|
}
|
|
|
|
const hitJobs = sr.detect({ name: 'X', careers_url: 'https://jobs.smartrecruiters.com/x' });
|
|
if (hitJobs && hitJobs.url.startsWith('https://api.smartrecruiters.com/v1/companies/x/postings')) {
|
|
pass('smartrecruiters.detect() also handles jobs.smartrecruiters.com');
|
|
} else {
|
|
fail(`smartrecruiters.detect(jobs) returned ${JSON.stringify(hitJobs)}`);
|
|
}
|
|
|
|
if (sr.detect({ name: 'X', careers_url: 'https://example.com/careers' }) === null) {
|
|
pass('smartrecruiters.detect() returns null for non-SR URLs');
|
|
} else {
|
|
fail('smartrecruiters.detect() should return null for non-SR URLs');
|
|
}
|
|
|
|
// entry.api precedence: a branded careers_url is kept while the SR slug is
|
|
// pinned via api: (mirrors greenhouse/ashby).
|
|
const hitApi = sr.detect({
|
|
name: 'Continental',
|
|
careers_url: 'https://jobs.continental.com',
|
|
api: 'https://careers.smartrecruiters.com/Continental',
|
|
});
|
|
if (hitApi && hitApi.url.startsWith('https://api.smartrecruiters.com/v1/companies/Continental/postings')) {
|
|
pass('smartrecruiters.detect() honors api: over a branded careers_url');
|
|
} else {
|
|
fail(`smartrecruiters.detect(api-pinned) returned ${JSON.stringify(hitApi)}`);
|
|
}
|
|
|
|
// parseSmartRecruitersResponse
|
|
const sample = {
|
|
content: [
|
|
{
|
|
id: 'abc-123',
|
|
name: 'Senior PM',
|
|
ref: 'https://api.smartrecruiters.com/v1/companies/sgs/postings/abc-123',
|
|
location: { fullLocation: 'Geneva, Switzerland', remote: false },
|
|
},
|
|
{
|
|
id: 'def-456',
|
|
name: 'Remote AI Engineer',
|
|
ref: 'https://api.smartrecruiters.com/v1/companies/sgs/postings/def-456',
|
|
location: { city: 'Paris', country: 'France', remote: true },
|
|
},
|
|
{
|
|
id: 'ghi-789',
|
|
name: 'No-ref Role',
|
|
location: { fullLocation: 'Berlin, Germany' },
|
|
},
|
|
],
|
|
};
|
|
const jobs = parseSmartRecruitersResponse(sample, 'SGS');
|
|
if (jobs.length === 3) pass('parseSmartRecruitersResponse extracts 3 jobs');
|
|
else fail(`parseSmartRecruitersResponse returned ${jobs.length} jobs`);
|
|
|
|
if (jobs[0]?.location === 'Geneva, Switzerland' && jobs[0]?.title === 'Senior PM') {
|
|
pass('parseSmartRecruitersResponse uses fullLocation when present');
|
|
} else {
|
|
fail(`row 0 = ${JSON.stringify(jobs[0])}`);
|
|
}
|
|
|
|
if (jobs[1]?.location === 'Paris, France, Remote') {
|
|
pass('parseSmartRecruitersResponse builds location from city/country/remote when no fullLocation');
|
|
} else {
|
|
fail(`row 1 location = ${JSON.stringify(jobs[1]?.location)}, expected "Paris, France, Remote"`);
|
|
}
|
|
|
|
if (jobs[0]?.url === 'https://jobs.smartrecruiters.com/sgs/postings/abc-123') {
|
|
pass('parseSmartRecruitersResponse rewrites api.smartrecruiters.com → jobs.smartrecruiters.com');
|
|
} else {
|
|
fail(`row 0 url = ${JSON.stringify(jobs[0]?.url)}`);
|
|
}
|
|
|
|
if (jobs[2]?.url && jobs[2].url.startsWith('https://jobs.smartrecruiters.com/sgs/ghi-789')) {
|
|
pass('parseSmartRecruitersResponse falls back to synthetic URL when ref is missing');
|
|
} else {
|
|
fail(`row 2 url = ${JSON.stringify(jobs[2]?.url)}`);
|
|
}
|
|
|
|
// Empty input safety
|
|
if (parseSmartRecruitersResponse({}, 'X').length === 0) pass('empty {} input → empty result');
|
|
else fail('empty {} input should yield empty result');
|
|
|
|
if (parseSmartRecruitersResponse({ content: 'not an array' }, 'X').length === 0) {
|
|
pass('non-array content → empty result (no crash)');
|
|
} else {
|
|
fail('non-array content should yield empty result');
|
|
}
|
|
|
|
// careers_url with non-string value → detect() returns null without crashing
|
|
if (sr.detect({ name: 'X', careers_url: { foo: 'bar' } }) === null) {
|
|
pass('smartrecruiters.detect() returns null for non-string careers_url (object)');
|
|
} else {
|
|
fail('smartrecruiters.detect() should treat non-string careers_url as missing');
|
|
}
|
|
|
|
// Fallback URL when both ref AND id are missing → empty string (not "undefined" in URL)
|
|
const noRefNoId = parseSmartRecruitersResponse(
|
|
{ content: [{ name: 'Stranded Role' }] },
|
|
'X',
|
|
);
|
|
if (noRefNoId.length === 1 && noRefNoId[0].url === '') {
|
|
pass('parseSmartRecruitersResponse returns url="" when both ref and id are missing');
|
|
} else {
|
|
fail(`expected url='' when ref+id both missing, got ${JSON.stringify(noRefNoId[0])}`);
|
|
}
|
|
|
|
// SSRF: malicious URL with smartrecruiters hostname in the PATH (not host) must not be detected.
|
|
if (sr.detect({ name: 'Spoof', careers_url: 'https://evil.example/careers.smartrecruiters.com/slug' }) === null) {
|
|
pass('smartrecruiters.detect() rejects path-spoofed URLs');
|
|
} else {
|
|
fail('smartrecruiters.detect() must NOT misdetect path-spoofed URLs');
|
|
}
|
|
|
|
// SmartRecruiters: untrusted j.ref host falls through to fallback rather than rewriting
|
|
const bogusRef = parseSmartRecruitersResponse(
|
|
{ content: [{ id: 'X1', name: 'Strange Role', ref: 'https://evil.example/v1/companies/x/postings/X1' }] },
|
|
'TestCo',
|
|
);
|
|
if (bogusRef[0]?.url && !bogusRef[0].url.includes('evil.example')) {
|
|
pass('parseSmartRecruitersResponse rejects untrusted j.ref host (falls through to fallback)');
|
|
} else {
|
|
fail(`untrusted j.ref leaked into url: ${JSON.stringify(bogusRef[0]?.url)}`);
|
|
}
|
|
|
|
// SmartRecruiters: companyName with spaces/symbols is slugified for the fallback URL
|
|
const slugifiedCompany = parseSmartRecruitersResponse(
|
|
{ content: [{ id: 'X2', name: 'Strange Role' }] },
|
|
'My Acme & Co.',
|
|
);
|
|
if (slugifiedCompany[0]?.url === 'https://jobs.smartrecruiters.com/my-acme-co/X2-strange-role') {
|
|
pass('parseSmartRecruitersResponse slugifies the companyName for the fallback URL');
|
|
} else {
|
|
fail(`fallback URL not properly slugified: ${JSON.stringify(slugifiedCompany[0]?.url)}`);
|
|
}
|
|
|
|
// Pagination: fetch() loops until an empty page (or short page) is returned
|
|
let pageRequests = 0;
|
|
const pagedJobs = await sr.fetch(
|
|
{ name: 'PagedCo', careers_url: 'https://careers.smartrecruiters.com/paged' },
|
|
{
|
|
transport: 'http',
|
|
fetchText: async () => { throw new Error('fetchText should not be called'); },
|
|
fetchJson: async (url) => {
|
|
pageRequests++;
|
|
const offset = parseInt(new URL(url).searchParams.get('offset') || '0', 10);
|
|
if (offset === 0) {
|
|
// Page 1: full page (100 items)
|
|
return { content: Array.from({ length: 100 }, (_, i) => ({ id: `P1-${i}`, name: `Role 1-${i}` })) };
|
|
}
|
|
if (offset === 100) {
|
|
// Page 2: short page (50 items) → loop stops after this
|
|
return { content: Array.from({ length: 50 }, (_, i) => ({ id: `P2-${i}`, name: `Role 2-${i}` })) };
|
|
}
|
|
// Should not be reached because page 2 was short
|
|
return { content: [] };
|
|
},
|
|
},
|
|
);
|
|
if (pageRequests === 2 && pagedJobs.length === 150) {
|
|
pass('smartrecruiters.fetch() paginates and aggregates results (2 pages → 150 total)');
|
|
} else {
|
|
fail(`pagination: pageRequests=${pageRequests}, total=${pagedJobs.length} (expected 2 requests / 150 results)`);
|
|
}
|
|
|
|
// Pagination stop condition: empty content terminates the loop
|
|
let emptyPageRequests = 0;
|
|
const emptyJobs = await sr.fetch(
|
|
{ name: 'EmptyCo', careers_url: 'https://careers.smartrecruiters.com/empty' },
|
|
{
|
|
transport: 'http',
|
|
fetchText: async () => { throw new Error('fetchText should not be called'); },
|
|
fetchJson: async () => {
|
|
emptyPageRequests++;
|
|
return { content: [] };
|
|
},
|
|
},
|
|
);
|
|
if (emptyPageRequests === 1 && emptyJobs.length === 0) {
|
|
pass('smartrecruiters.fetch() stops on the first empty page');
|
|
} else {
|
|
fail(`empty pagination: requests=${emptyPageRequests}, total=${emptyJobs.length}`);
|
|
}
|
|
|
|
} catch (e) {
|
|
fail(`smartrecruiters provider tests crashed: ${e.message}`);
|
|
}
|
|
|