202 lines
5.1 KiB
JavaScript
202 lines
5.1 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { createCrawlerClient } = require("../src/crawlerClient");
|
|
|
|
function createJsonResponse(body, status = 200) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
async json() {
|
|
return body;
|
|
},
|
|
async text() {
|
|
return JSON.stringify(body);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createTextResponse(text, status = 500) {
|
|
return {
|
|
ok: false,
|
|
status,
|
|
async json() {
|
|
throw new Error("json not available");
|
|
},
|
|
async text() {
|
|
return text;
|
|
},
|
|
};
|
|
}
|
|
|
|
test("single endpoint mode still works", async () => {
|
|
const crawler = createCrawlerClient({
|
|
endpoint: "https://single-endpoint.example",
|
|
maxAttempts: 1,
|
|
fetch: async () =>
|
|
createJsonResponse({
|
|
currency: "KRW",
|
|
offers: [{ provider: "single", price: 123456 }],
|
|
}),
|
|
});
|
|
|
|
const offers = await crawler.getQuotes({
|
|
watchId: "watch-1",
|
|
searchParams: { segments: [{ from: "ICN", to: "MAD" }] },
|
|
});
|
|
|
|
assert.equal(offers.length, 1);
|
|
assert.equal(offers[0].provider, "single");
|
|
assert.equal(offers[0].price, 123456);
|
|
});
|
|
|
|
test("endpoint mode retries transient failures and then succeeds", async () => {
|
|
let calls = 0;
|
|
const crawler = createCrawlerClient({
|
|
endpoint: "https://retry-endpoint.example",
|
|
maxAttempts: 3,
|
|
retryBaseDelayMs: 1,
|
|
retryMaxDelayMs: 1,
|
|
fetch: async () => {
|
|
calls += 1;
|
|
if (calls < 3) {
|
|
return createTextResponse("upstream failed", 503);
|
|
}
|
|
return createJsonResponse({
|
|
currency: "KRW",
|
|
offers: [{ provider: "retry", price: 777000 }],
|
|
});
|
|
},
|
|
});
|
|
|
|
const offers = await crawler.getQuotes({
|
|
watchId: "watch-retry",
|
|
searchParams: { segments: [{ from: "ICN", to: "MAD" }] },
|
|
});
|
|
|
|
assert.equal(calls, 3);
|
|
assert.equal(offers.length, 1);
|
|
assert.equal(offers[0].provider, "retry");
|
|
});
|
|
|
|
test("priorityFallback tries next provider when primary fails", async () => {
|
|
const calls = [];
|
|
const crawler = createCrawlerClient({
|
|
endpoint: null,
|
|
providers: ["skyscanner", "naver"],
|
|
providerEndpoints: {
|
|
skyscanner: "https://skyscanner.example",
|
|
naver: "https://naver.example",
|
|
},
|
|
routingStrategy: "priorityFallback",
|
|
maxAttempts: 1,
|
|
fetch: async (url) => {
|
|
calls.push(url);
|
|
if (url.includes("skyscanner")) {
|
|
return createTextResponse("upstream failed", 503);
|
|
}
|
|
return createJsonResponse({
|
|
currency: "KRW",
|
|
offers: [{ price: 980000 }],
|
|
});
|
|
},
|
|
});
|
|
|
|
const offers = await crawler.getQuotes({
|
|
watchId: "watch-2",
|
|
searchParams: { segments: [{ from: "ICN", to: "MAD" }] },
|
|
});
|
|
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(offers.length, 1);
|
|
assert.equal(offers[0].provider, "naver");
|
|
assert.equal(offers[0].price, 980000);
|
|
});
|
|
|
|
test("primaryOnly does not fallback to the next provider", async () => {
|
|
const calls = [];
|
|
const crawler = createCrawlerClient({
|
|
endpoint: null,
|
|
providers: ["skyscanner", "naver"],
|
|
providerEndpoints: {
|
|
skyscanner: "https://skyscanner.example",
|
|
naver: "https://naver.example",
|
|
},
|
|
routingStrategy: "primaryOnly",
|
|
maxAttempts: 1,
|
|
fetch: async (url) => {
|
|
calls.push(url);
|
|
if (url.includes("skyscanner")) {
|
|
return createTextResponse("timeout", 504);
|
|
}
|
|
return createJsonResponse({
|
|
currency: "KRW",
|
|
offers: [{ price: 970000 }],
|
|
});
|
|
},
|
|
});
|
|
|
|
await assert.rejects(async () => crawler.getQuotes({ watchId: "watch-3", searchParams: {} }), {
|
|
message: /Primary provider failed/,
|
|
});
|
|
assert.equal(calls.length, 1);
|
|
});
|
|
|
|
test("parallelRace returns first successful provider result", async () => {
|
|
const crawler = createCrawlerClient({
|
|
endpoint: null,
|
|
providers: ["skyscanner", "naver"],
|
|
providerEndpoints: {
|
|
skyscanner: "https://skyscanner.example",
|
|
naver: "https://naver.example",
|
|
},
|
|
routingStrategy: "parallelRace",
|
|
maxAttempts: 1,
|
|
fetch: async (url) =>
|
|
new Promise((resolve) => {
|
|
if (url.includes("naver")) {
|
|
setTimeout(() => {
|
|
resolve(
|
|
createJsonResponse({
|
|
currency: "KRW",
|
|
offers: [{ price: 930000 }],
|
|
})
|
|
);
|
|
}, 5);
|
|
return;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
resolve(
|
|
createJsonResponse({
|
|
currency: "KRW",
|
|
offers: [{ price: 950000 }],
|
|
})
|
|
);
|
|
}, 40);
|
|
}),
|
|
});
|
|
|
|
const offers = await crawler.getQuotes({
|
|
watchId: "watch-4",
|
|
searchParams: { segments: [{ from: "ICN", to: "MAD" }] },
|
|
});
|
|
|
|
assert.equal(offers.length, 1);
|
|
assert.equal(offers[0].provider, "naver");
|
|
assert.equal(offers[0].price, 930000);
|
|
});
|
|
|
|
test("throws when multi-source providers are configured without endpoints", () => {
|
|
assert.throws(
|
|
() =>
|
|
createCrawlerClient({
|
|
endpoint: null,
|
|
providers: ["unconfigured-provider"],
|
|
providerEndpoints: {},
|
|
}),
|
|
/Missing endpoint for provider\(s\): unconfigured-provider/
|
|
);
|
|
});
|