Files
air-watcher/test/crawlerClient.test.js
2026-02-19 17:28:58 +09:00

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/
);
});