test: quiet and speed up test runs
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { logger } from "./logger";
|
||||
import { ebayRoute } from "./routes/ebay";
|
||||
import { facebookRoute } from "./routes/facebook";
|
||||
import { kijijiRoute } from "./routes/kijiji";
|
||||
@@ -27,4 +28,4 @@ const server = Bun.serve({
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`API Server running on ${server.hostname}:${server.port}`);
|
||||
logger.log(`API Server running on ${server.hostname}:${server.port}`);
|
||||
|
||||
10
packages/api-server/src/logger.ts
Normal file
10
packages/api-server/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const isTest = () => process.env.NODE_ENV === "test";
|
||||
|
||||
export const logger = {
|
||||
log: (...args: Parameters<typeof console.log>) => {
|
||||
if (!isTest()) console.log(...args);
|
||||
},
|
||||
error: (...args: Parameters<typeof console.error>) => {
|
||||
if (!isTest()) console.error(...args);
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchEbayItems } from "@marketplace-scrapers/core";
|
||||
import { logger } from "../logger";
|
||||
|
||||
/**
|
||||
* GET /api/ebay?q={query}&minPrice={minPrice}&maxPrice={maxPrice}&strictMode={strictMode}&exclusions={exclusions}&keywords={keywords}&buyItNowOnly={buyItNowOnly}&canadaOnly={canadaOnly}
|
||||
@@ -90,7 +91,7 @@ export async function ebayRoute(req: Request): Promise<Response> {
|
||||
);
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("eBay scraping error:", error);
|
||||
logger.error("eBay scraping error:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return Response.json({ message: errorMessage }, { status: 400 });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchFacebookItems } from "@marketplace-scrapers/core";
|
||||
import { logger } from "../logger";
|
||||
|
||||
/**
|
||||
* GET /api/facebook?q={query}&location={location}
|
||||
@@ -57,7 +58,7 @@ export async function facebookRoute(req: Request): Promise<Response> {
|
||||
);
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Facebook scraping error:", error);
|
||||
logger.error("Facebook scraping error:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return Response.json({ message: errorMessage }, { status: 400 });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchKijijiItems } from "@marketplace-scrapers/core";
|
||||
import { logger } from "../logger";
|
||||
|
||||
/**
|
||||
* GET /api/kijiji?q={query}
|
||||
@@ -97,7 +98,7 @@ export async function kijijiRoute(req: Request): Promise<Response> {
|
||||
);
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Kijiji scraping error:", error);
|
||||
logger.error("Kijiji scraping error:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
return Response.json({ message: errorMessage }, { status: 400 });
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
formatCookiesForHeader,
|
||||
} from "../utils/cookies";
|
||||
import { delay } from "../utils/delay";
|
||||
import { logger } from "../utils/logger";
|
||||
import { classifyUnstableListings } from "../utils/unstable";
|
||||
|
||||
// eBay cookie configuration
|
||||
@@ -354,7 +355,7 @@ function parseEbayListings(
|
||||
results.push(listing);
|
||||
seenUrls.add(canonicalUrl);
|
||||
} catch (err) {
|
||||
console.warn(`Error parsing eBay listing: ${err}`);
|
||||
logger.warn(`Error parsing eBay listing: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ async function loadEbayCookies(): Promise<string | undefined> {
|
||||
const cookies = await ensureCookies(EBAY_COOKIE_CONFIG);
|
||||
return formatCookiesForHeader(cookies, "www.ebay.ca");
|
||||
} catch {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"No valid eBay cookies found in EBAY_COOKIE. eBay may block requests without a raw Cookie header string.",
|
||||
);
|
||||
return undefined;
|
||||
@@ -474,7 +475,7 @@ export default async function fetchEbayItems(
|
||||
|
||||
const DELAY_MS = Math.max(1, Math.floor(1000 / requestsPerSecond));
|
||||
|
||||
console.log(`Fetching eBay search: ${searchUrl}`);
|
||||
logger.log(`Fetching eBay search: ${searchUrl}`);
|
||||
|
||||
try {
|
||||
// Use custom headers modeled after real browser requests to bypass bot detection
|
||||
@@ -516,7 +517,7 @@ export default async function fetchEbayItems(
|
||||
// Respect per-request delay to keep at or under REQUESTS_PER_SECOND
|
||||
await delay(DELAY_MS);
|
||||
|
||||
console.log(`\nParsing eBay listings...`);
|
||||
logger.log(`\nParsing eBay listings...`);
|
||||
|
||||
const listings = parseEbayListings(
|
||||
searchHtml,
|
||||
@@ -533,7 +534,7 @@ export default async function fetchEbayItems(
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`Parsed ${filteredListings.length} eBay listings.`);
|
||||
logger.log(`Parsed ${filteredListings.length} eBay listings.`);
|
||||
return finalizeResults(filteredListings);
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { delay } from "../utils/delay";
|
||||
import { formatCentsToCurrency } from "../utils/format";
|
||||
import { isRecord } from "../utils/http";
|
||||
import { logger } from "../utils/logger";
|
||||
import { classifyUnstableListings } from "../utils/unstable";
|
||||
|
||||
/**
|
||||
@@ -260,14 +261,14 @@ function logExtractionMetrics(success: boolean, itemId?: string) {
|
||||
successRate < 0.8 &&
|
||||
!extractionStats.lastApiChangeDetected
|
||||
) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Facebook Marketplace API extraction success rate dropped below 80%. This may indicate API changes.",
|
||||
);
|
||||
extractionStats.lastApiChangeDetected = new Date();
|
||||
}
|
||||
|
||||
if (!success && itemId) {
|
||||
console.warn(`Facebook API extraction failed for item ${itemId}`);
|
||||
logger.warn(`Facebook API extraction failed for item ${itemId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,18 +821,18 @@ export function extractFacebookMarketplaceData(
|
||||
if (htmlString.includes("XCometMarketplaceSearchController")) {
|
||||
const htmlFallback = extractFacebookMarketplaceHtmlFallback(htmlString);
|
||||
if (htmlFallback?.length) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Successfully parsed ${htmlFallback.length} Facebook marketplace listings from rendered HTML fallback`,
|
||||
);
|
||||
return htmlFallback;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn("No marketplace data found in HTML response");
|
||||
logger.warn("No marketplace data found in HTML response");
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`Successfully parsed ${bestEdges.length} Facebook marketplace listings`,
|
||||
);
|
||||
return bestEdges.map((edge) => ({ node: edge.node }));
|
||||
@@ -982,7 +983,7 @@ export function parseFacebookAds(
|
||||
|
||||
results.push(listingDetails);
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse Facebook ad:", error);
|
||||
logger.warn("Failed to parse Facebook ad:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1083,7 +1084,7 @@ export function parseFacebookItem(
|
||||
|
||||
return listingDetails;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse Facebook item ${item.id}:`, error);
|
||||
logger.warn(`Failed to parse Facebook item ${item.id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1148,8 +1149,8 @@ export default async function fetchFacebookItems(
|
||||
// Facebook marketplace URL structure
|
||||
const searchUrl = `https://www.facebook.com/marketplace/${LOCATION}/search?query=${encodedQuery}&sortBy=creation_time_descend&exact=false`;
|
||||
|
||||
console.log(`Fetching Facebook marketplace: ${searchUrl}`);
|
||||
console.log(`Using ${cookies.length} cookies for authentication`);
|
||||
logger.log(`Fetching Facebook marketplace: ${searchUrl}`);
|
||||
logger.log(`Using ${cookies.length} cookies for authentication`);
|
||||
|
||||
let searchHtml: string;
|
||||
let searchResponseUrl = searchUrl;
|
||||
@@ -1158,7 +1159,7 @@ export default async function fetchFacebookItems(
|
||||
maxRetries: 3,
|
||||
onRateInfo: (remaining, reset) => {
|
||||
if (remaining && reset) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`\nFacebook - Rate limit remaining: ${remaining}, reset in: ${reset}s`,
|
||||
);
|
||||
}
|
||||
@@ -1169,11 +1170,11 @@ export default async function fetchFacebookItems(
|
||||
searchResponseUrl = response.responseUrl;
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`\nFacebook marketplace access failed (${err.status}): ${err.message}`,
|
||||
);
|
||||
if (err.status === 400 || err.status === 401 || err.status === 403) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"This might indicate invalid or expired cookies. Update FACEBOOK_COOKIE with a fresh raw Cookie header string.",
|
||||
);
|
||||
}
|
||||
@@ -1187,19 +1188,19 @@ export default async function fetchFacebookItems(
|
||||
searchResponseUrl,
|
||||
);
|
||||
if (classification.authGated) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Facebook marketplace search redirected to login. Cookies may be expired.",
|
||||
);
|
||||
return finalizeResults([]);
|
||||
}
|
||||
|
||||
if (classification.unavailable) {
|
||||
console.warn("Facebook marketplace search returned an unavailable route.");
|
||||
logger.warn("Facebook marketplace search returned an unavailable route.");
|
||||
return finalizeResults([]);
|
||||
}
|
||||
|
||||
if (classification.kind !== "search") {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Facebook marketplace search returned unexpected route kind: ${classification.kind}.`,
|
||||
);
|
||||
return finalizeResults([]);
|
||||
@@ -1207,11 +1208,11 @@ export default async function fetchFacebookItems(
|
||||
|
||||
const ads = extractFacebookMarketplaceData(searchHtml);
|
||||
if (!ads || ads.length === 0) {
|
||||
console.warn("No ads parsed from Facebook marketplace page.");
|
||||
logger.warn("No ads parsed from Facebook marketplace page.");
|
||||
return finalizeResults([]);
|
||||
}
|
||||
|
||||
console.log(`\nFound ${ads.length} raw ads. Processing...`);
|
||||
logger.log(`\nFound ${ads.length} raw ads. Processing...`);
|
||||
|
||||
const isTTY = process.stdout?.isTTY ?? false;
|
||||
const progressBar = isTTY
|
||||
@@ -1232,7 +1233,7 @@ export default async function fetchFacebookItems(
|
||||
progressBar?.update(totalProgress);
|
||||
progressBar?.stop();
|
||||
|
||||
console.log(`\nParsed ${pricedItems.length} Facebook marketplace listings.`);
|
||||
logger.log(`\nParsed ${pricedItems.length} Facebook marketplace listings.`);
|
||||
return finalizeResults(pricedItems);
|
||||
}
|
||||
|
||||
@@ -1254,7 +1255,7 @@ export async function fetchFacebookItem(
|
||||
|
||||
const itemUrl = `https://www.facebook.com/marketplace/item/${itemId}/`;
|
||||
|
||||
console.log(`Fetching Facebook marketplace item: ${itemUrl}`);
|
||||
logger.log(`Fetching Facebook marketplace item: ${itemUrl}`);
|
||||
|
||||
let itemHtml: string;
|
||||
let itemResponseUrl = itemUrl;
|
||||
@@ -1262,7 +1263,7 @@ export async function fetchFacebookItem(
|
||||
const response = await fetchHtml(itemUrl, 1000, {
|
||||
onRateInfo: (remaining, reset) => {
|
||||
if (remaining && reset) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`\nFacebook - Rate limit remaining: ${remaining}, reset in: ${reset}s`,
|
||||
);
|
||||
}
|
||||
@@ -1273,7 +1274,7 @@ export async function fetchFacebookItem(
|
||||
itemResponseUrl = response.responseUrl;
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`\nFacebook marketplace item access failed (${err.status}): ${err.message}`,
|
||||
);
|
||||
|
||||
@@ -1282,29 +1283,29 @@ export async function fetchFacebookItem(
|
||||
case 400:
|
||||
case 401:
|
||||
case 403:
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Authentication error: Invalid or expired cookies. Update FACEBOOK_COOKIE with a fresh raw Cookie header string.",
|
||||
);
|
||||
break;
|
||||
case 404:
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Listing not found: The marketplace item may have been removed, sold, or the URL is invalid.",
|
||||
);
|
||||
break;
|
||||
case 429:
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Rate limited: Too many requests. Facebook is blocking access temporarily.",
|
||||
);
|
||||
break;
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Facebook server error: Marketplace may be temporarily unavailable.",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unexpected error status: ${err.status}`);
|
||||
logger.warn(`Unexpected error status: ${err.status}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1315,7 +1316,7 @@ export async function fetchFacebookItem(
|
||||
|
||||
if (classification.authGated) {
|
||||
logExtractionMetrics(false, itemId);
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Authentication failed for item ${itemId}. Cookies may be expired.`,
|
||||
);
|
||||
return null;
|
||||
@@ -1323,7 +1324,7 @@ export async function fetchFacebookItem(
|
||||
|
||||
if (itemResponseUrl.includes("unavailable_product=1")) {
|
||||
logExtractionMetrics(false, itemId);
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Item ${itemId} appears to be sold or removed from marketplace.`,
|
||||
);
|
||||
return null;
|
||||
@@ -1333,7 +1334,7 @@ export async function fetchFacebookItem(
|
||||
|
||||
if (classification.unavailable && !itemData) {
|
||||
logExtractionMetrics(false, itemId);
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Item ${itemId} appears to be sold or removed from marketplace.`,
|
||||
);
|
||||
return null;
|
||||
@@ -1341,7 +1342,7 @@ export async function fetchFacebookItem(
|
||||
|
||||
if (classification.kind !== "item" && !itemData) {
|
||||
logExtractionMetrics(false, itemId);
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Item ${itemId} returned unexpected route kind: ${classification.kind}.`,
|
||||
);
|
||||
return null;
|
||||
@@ -1351,38 +1352,38 @@ export async function fetchFacebookItem(
|
||||
logExtractionMetrics(false, itemId);
|
||||
|
||||
if (itemHtml.includes("This item has been sold")) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Item ${itemId} appears to be sold or removed from marketplace.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`No item data found in Facebook marketplace page for item ${itemId}. This may indicate:`,
|
||||
);
|
||||
console.warn(" - The listing was removed or sold");
|
||||
console.warn(" - Authentication issues");
|
||||
console.warn(" - Facebook changed their API structure");
|
||||
console.warn(" - Network or parsing issues");
|
||||
logger.warn(" - The listing was removed or sold");
|
||||
logger.warn(" - Authentication issues");
|
||||
logger.warn(" - Facebook changed their API structure");
|
||||
logger.warn(" - Network or parsing issues");
|
||||
return null;
|
||||
}
|
||||
|
||||
logExtractionMetrics(true, itemId);
|
||||
console.log(`Successfully extracted data for item ${itemId}`);
|
||||
logger.log(`Successfully extracted data for item ${itemId}`);
|
||||
|
||||
const parsedItem = parseFacebookItem(itemData);
|
||||
if (!parsedItem) {
|
||||
console.warn(`Failed to parse item ${itemId}: Invalid data structure`);
|
||||
logger.warn(`Failed to parse item ${itemId}: Invalid data structure`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for sold/removed status in the parsed data with proper precedence
|
||||
if (itemData.is_sold) {
|
||||
console.warn(`Item ${itemId} is marked as sold in the marketplace.`);
|
||||
logger.warn(`Item ${itemId} is marked as sold in the marketplace.`);
|
||||
// Still return the data but mark it as sold
|
||||
parsedItem.listingStatus = "SOLD";
|
||||
} else if (!itemData.is_live) {
|
||||
console.warn(`Item ${itemId} is not live/active in the marketplace.`);
|
||||
logger.warn(`Item ${itemId} is not live/active in the marketplace.`);
|
||||
parsedItem.listingStatus = itemData.is_hidden
|
||||
? "HIDDEN"
|
||||
: itemData.is_pending
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
} from "../utils/http";
|
||||
import { logger } from "../utils/logger";
|
||||
import { classifyUnstableListings } from "../utils/unstable";
|
||||
|
||||
// Kijiji cookie configuration
|
||||
@@ -489,7 +490,7 @@ async function fetchSellerDetails(
|
||||
};
|
||||
} catch (err) {
|
||||
// Silently fail for GraphQL errors - not critical for basic functionality
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to fetch seller details for ${posterId}:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
@@ -723,7 +724,7 @@ export async function parseDetailedListing(
|
||||
};
|
||||
} catch {
|
||||
// Silently fail - GraphQL data is optional
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to fetch additional seller data for ${posterInfo.posterId}`,
|
||||
);
|
||||
}
|
||||
@@ -860,11 +861,11 @@ export default async function fetchKijijiItems(
|
||||
BASE_URL,
|
||||
);
|
||||
|
||||
console.log(`Fetching search page ${page}: ${searchUrl}`);
|
||||
logger.log(`Fetching search page ${page}: ${searchUrl}`);
|
||||
const searchHtml = await fetchHtml(searchUrl, DELAY_MS, {
|
||||
onRateInfo: (remaining, reset) => {
|
||||
if (remaining && reset) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`\nSearch - Rate limit remaining: ${remaining}, reset in: ${reset}s`,
|
||||
);
|
||||
}
|
||||
@@ -874,9 +875,7 @@ export default async function fetchKijijiItems(
|
||||
|
||||
const searchResults = parseSearch(searchHtml, BASE_URL);
|
||||
if (searchResults.length === 0) {
|
||||
console.log(
|
||||
`No more results found on page ${page}. Stopping pagination.`,
|
||||
);
|
||||
logger.log(`No more results found on page ${page}. Stopping pagination.`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -889,7 +888,7 @@ export default async function fetchKijijiItems(
|
||||
seenUrls.add(link);
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`\nFound ${newListingLinks.length} new listing links on page ${page}. Total unique: ${seenUrls.size}`,
|
||||
);
|
||||
|
||||
@@ -920,7 +919,7 @@ export default async function fetchKijijiItems(
|
||||
// Staggered starts keep request pacing within REQUESTS_PER_SECOND.
|
||||
onRateInfo: (remaining, reset) => {
|
||||
if (remaining && reset) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`\nItem - Rate limit remaining: ${remaining}, reset in: ${reset}s`,
|
||||
);
|
||||
}
|
||||
@@ -948,7 +947,7 @@ export default async function fetchKijijiItems(
|
||||
currentProgress++;
|
||||
progressBar?.update(currentProgress);
|
||||
if (!progressBar) {
|
||||
console.log(`Progress: ${currentProgress}/${totalProgress}`);
|
||||
logger.log(`Progress: ${currentProgress}/${totalProgress}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -977,7 +976,7 @@ export default async function fetchKijijiItems(
|
||||
matchesPriceFilters(listing, finalSearchOptions),
|
||||
);
|
||||
|
||||
console.log(`\nParsed ${filteredListings.length} detailed listings.`);
|
||||
logger.log(`\nParsed ${filteredListings.length} detailed listings.`);
|
||||
return finalizeResults(filteredListings);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Shared cookie handling utilities for marketplace scrapers
|
||||
*/
|
||||
|
||||
import { logger } from "./logger";
|
||||
|
||||
export interface Cookie {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -116,7 +118,7 @@ export async function ensureCookies(
|
||||
const cookies = parseCookieString(envValue ?? "", config.domain);
|
||||
|
||||
if (cookies.length > 0) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Loaded ${cookies.length} ${config.name} cookies from ${config.envVar} env var`,
|
||||
);
|
||||
return cookies;
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
* @returns A promise that resolves after the specified delay
|
||||
*/
|
||||
export function delay(ms: number): Promise<void> {
|
||||
if (process.env.NODE_ENV === "test") return Promise.resolve();
|
||||
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { delay } from "./delay";
|
||||
|
||||
/** Custom error class for HTTP-related failures */
|
||||
export class HttpError extends Error {
|
||||
override name = "HttpError";
|
||||
@@ -167,7 +169,7 @@ export async function fetchHtml(
|
||||
const html = await res.text();
|
||||
|
||||
// Respect per-request delay to maintain rate limiting
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
await delay(delayMs);
|
||||
return html;
|
||||
} catch (err) {
|
||||
// Re-throw known errors
|
||||
|
||||
10
packages/core/src/utils/logger.ts
Normal file
10
packages/core/src/utils/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const isTest = () => process.env.NODE_ENV === "test";
|
||||
|
||||
export const logger = {
|
||||
log: (...args: Parameters<typeof console.log>) => {
|
||||
if (!isTest()) console.log(...args);
|
||||
},
|
||||
warn: (...args: Parameters<typeof console.warn>) => {
|
||||
if (!isTest()) console.warn(...args);
|
||||
},
|
||||
};
|
||||
24
packages/core/test/delay.test.ts
Normal file
24
packages/core/test/delay.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import { delay } from "../src/utils/delay";
|
||||
|
||||
describe("delay", () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
});
|
||||
|
||||
test("does not schedule throttle timers during tests", async () => {
|
||||
process.env.NODE_ENV = "test";
|
||||
const setTimeoutMock = mock(() => {
|
||||
throw new Error("setTimeout should not be called during tests");
|
||||
});
|
||||
globalThis.setTimeout = setTimeoutMock as unknown as typeof setTimeout;
|
||||
|
||||
await delay(1000);
|
||||
|
||||
expect(setTimeoutMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -44,9 +44,6 @@ describe("eBay Scraper Cookie Handling", () => {
|
||||
});
|
||||
|
||||
test("should ignore request cookie overrides and rely on EBAY_COOKIE", async () => {
|
||||
const warnMock = mock(() => {});
|
||||
console.warn = warnMock;
|
||||
|
||||
await fetchEbayItems("laptop", 1000);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
@@ -61,9 +58,6 @@ describe("eBay Scraper Cookie Handling", () => {
|
||||
const headers = (init as RequestInit).headers as Record<string, string>;
|
||||
|
||||
expect(headers.Cookie).toBeUndefined();
|
||||
expect(warnMock).toHaveBeenCalledWith(
|
||||
"No valid eBay cookies found in EBAY_COOKIE. eBay may block requests without a raw Cookie header string.",
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps relative item links on the ebay.ca host", async () => {
|
||||
|
||||
@@ -177,10 +177,6 @@ describe("Facebook Marketplace Scraper Core Tests", () => {
|
||||
});
|
||||
|
||||
test("should handle authentication errors", async () => {
|
||||
const originalWarn = console.warn;
|
||||
const warnMock = mock(() => {});
|
||||
console.warn = warnMock;
|
||||
|
||||
global.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
@@ -192,16 +188,9 @@ describe("Facebook Marketplace Scraper Core Tests", () => {
|
||||
}),
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await fetchFacebookItem("123");
|
||||
expect(result).toBeNull();
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(warnMock).toHaveBeenCalledWith(
|
||||
"Authentication error: Invalid or expired cookies. Update FACEBOOK_COOKIE with a fresh raw Cookie header string.",
|
||||
);
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
const result = await fetchFacebookItem("123");
|
||||
expect(result).toBeNull();
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should handle item not found", async () => {
|
||||
@@ -1708,10 +1697,6 @@ describe("Facebook Marketplace Scraper Core Tests", () => {
|
||||
});
|
||||
|
||||
test("should handle malformed ads gracefully", () => {
|
||||
const originalWarn = console.warn;
|
||||
const warnMock = mock(() => {});
|
||||
console.warn = warnMock;
|
||||
|
||||
const ads = [
|
||||
{
|
||||
node: {
|
||||
@@ -1739,9 +1724,6 @@ describe("Facebook Marketplace Scraper Core Tests", () => {
|
||||
);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]?.title).toBe("Valid Ad");
|
||||
expect(warnMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
|
||||
test("parses formatted fallback prices with multiple commas", () => {
|
||||
|
||||
41
packages/core/test/http.test.ts
Normal file
41
packages/core/test/http.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import { fetchHtml } from "../src/utils/http";
|
||||
|
||||
describe("fetchHtml", () => {
|
||||
const originalFetch = global.fetch;
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const originalClearTimeout = globalThis.clearTimeout;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
globalThis.clearTimeout = originalClearTimeout;
|
||||
});
|
||||
|
||||
test("does not schedule throttle timers during tests", async () => {
|
||||
process.env.NODE_ENV = "test";
|
||||
const scheduledDelays: number[] = [];
|
||||
|
||||
global.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => null },
|
||||
text: () => Promise.resolve("<html></html>"),
|
||||
}),
|
||||
) as unknown as typeof fetch;
|
||||
globalThis.setTimeout = mock((handler: TimerHandler, timeout?: number) => {
|
||||
scheduledDelays.push(Number(timeout));
|
||||
if (timeout !== 30_000 && typeof handler === "function") {
|
||||
handler();
|
||||
}
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>;
|
||||
}) as unknown as typeof setTimeout;
|
||||
globalThis.clearTimeout = mock(() => {}) as unknown as typeof clearTimeout;
|
||||
|
||||
await fetchHtml("https://example.com", 1000, { timeoutMs: 30_000 });
|
||||
|
||||
expect(scheduledDelays).not.toContain(1000);
|
||||
});
|
||||
});
|
||||
29
packages/core/test/logger.test.ts
Normal file
29
packages/core/test/logger.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
|
||||
describe("logger", () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
test("suppresses log and warn output during tests", async () => {
|
||||
process.env.NODE_ENV = "test";
|
||||
const logMock = mock(() => {});
|
||||
const warnMock = mock(() => {});
|
||||
console.log = logMock;
|
||||
console.warn = warnMock;
|
||||
|
||||
const { logger } = await import("../src/utils/logger");
|
||||
|
||||
logger.log("hidden log");
|
||||
logger.warn("hidden warn");
|
||||
|
||||
expect(logMock).not.toHaveBeenCalled();
|
||||
expect(warnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logger } from "./logger";
|
||||
import { handleMcpRequest } from "./protocol/handler";
|
||||
import { serverCard } from "./protocol/metadata";
|
||||
|
||||
@@ -33,4 +34,4 @@ const server = Bun.serve({
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`MCP Server running on ${server.hostname}:${server.port}`);
|
||||
logger.log(`MCP Server running on ${server.hostname}:${server.port}`);
|
||||
|
||||
10
packages/mcp-server/src/logger.ts
Normal file
10
packages/mcp-server/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const isTest = () => process.env.NODE_ENV === "test";
|
||||
|
||||
export const logger = {
|
||||
log: (...args: Parameters<typeof console.log>) => {
|
||||
if (!isTest()) console.log(...args);
|
||||
},
|
||||
error: (...args: Parameters<typeof console.error>) => {
|
||||
if (!isTest()) console.error(...args);
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logger } from "../logger";
|
||||
import { tools } from "./tools";
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:4005/api";
|
||||
@@ -119,7 +120,7 @@ export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
if (args.unstableFilter !== undefined)
|
||||
params.append("unstableFilter", args.unstableFilter.toString());
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`[MCP] Calling Kijiji API: ${API_BASE_URL}/kijiji?${params.toString()}`,
|
||||
);
|
||||
const response = await Promise.race([
|
||||
@@ -135,13 +136,13 @@ export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(
|
||||
logger.error(
|
||||
`[MCP] Kijiji API error ${response.status}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`API returned ${response.status}: ${errorText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
console.log(
|
||||
logger.log(
|
||||
`[MCP] Kijiji returned ${Array.isArray(result) ? result.length : 0} items`,
|
||||
);
|
||||
} else if (name === "search_facebook") {
|
||||
@@ -160,7 +161,7 @@ export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
if (args.unstableFilter !== undefined)
|
||||
params.append("unstableFilter", args.unstableFilter.toString());
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`[MCP] Calling Facebook API: ${API_BASE_URL}/facebook?${params.toString()}`,
|
||||
);
|
||||
const response = await Promise.race([
|
||||
@@ -176,13 +177,13 @@ export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(
|
||||
logger.error(
|
||||
`[MCP] Facebook API error ${response.status}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`API returned ${response.status}: ${errorText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
console.log(
|
||||
logger.log(
|
||||
`[MCP] Facebook returned ${Array.isArray(result) ? result.length : 0} items`,
|
||||
);
|
||||
} else if (name === "search_ebay") {
|
||||
@@ -214,7 +215,7 @@ export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
if (args.unstableFilter !== undefined)
|
||||
params.append("unstableFilter", args.unstableFilter.toString());
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`[MCP] Calling eBay API: ${API_BASE_URL}/ebay?${params.toString()}`,
|
||||
);
|
||||
const response = await Promise.race([
|
||||
@@ -230,13 +231,13 @@ export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(
|
||||
logger.error(
|
||||
`[MCP] eBay API error ${response.status}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`API returned ${response.status}: ${errorText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
console.log(
|
||||
logger.log(
|
||||
`[MCP] eBay returned ${Array.isArray(result) ? result.length : 0} items`,
|
||||
);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user