initial commit
This commit is contained in:
201
test/crawlerClient.test.js
Normal file
201
test/crawlerClient.test.js
Normal file
@@ -0,0 +1,201 @@
|
||||
"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/
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user