import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import fetchFacebookItems, { fetchFacebookItem } from "../src/scrapers/facebook"; // Mock fetch globally const originalFetch = global.fetch; describe("Facebook Marketplace Scraper Integration Tests", () => { beforeEach(() => { global.fetch = mock(() => { throw new Error("fetch should be mocked in individual tests"); }); }); afterEach(() => { global.fetch = originalFetch; }); describe("Main Search Function", () => { const mockCookies = JSON.stringify([ { name: "c_user", value: "12345", domain: ".facebook.com", path: "/" }, { name: "xs", value: "abc123", domain: ".facebook.com", path: "/" }, ]); test("should successfully fetch search results", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "iPhone 13 Pro", listing_price: { amount: "800.00", formatted_amount: "$800.00", currency: "CAD", }, location: { reverse_geocode: { city_page: { display_name: "Toronto" }, }, }, creation_time: 1640995200, is_live: true, }, }, }, { node: { listing: { id: "2", marketplace_listing_title: "Samsung Galaxy", listing_price: { amount: "600.00", formatted_amount: "$600.00", currency: "CAD", }, location: { reverse_geocode: { city_page: { display_name: "Mississauga" }, }, }, creation_time: 1640995300, is_live: true, }, }, }, ], }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( `
`, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "iPhone", 1, "toronto", 25, mockCookies, ); expect(results).toHaveLength(2); expect(results[0].title).toBe("iPhone 13 Pro"); expect(results[1].title).toBe("Samsung Galaxy"); }); test("should filter out items without price", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "With Price", listing_price: { amount: "100.00", formatted_amount: "$100.00", currency: "CAD", }, is_live: true, }, }, }, { node: { listing: { id: "2", marketplace_listing_title: "No Price", is_live: true, }, }, }, ], }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "test", 1, "toronto", 25, mockCookies, ); expect(results).toHaveLength(1); expect(results[0].title).toBe("With Price"); }); test("should respect MAX_ITEMS parameter", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: Array.from({ length: 10 }, (_, i) => ({ node: { listing: { id: String(i), marketplace_listing_title: `Item ${i}`, listing_price: { amount: `${(i + 1) * 10}.00`, formatted_amount: `$${(i + 1) * 10}.00`, currency: "CAD", }, is_live: true, }, }, })), }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "test", 1, "toronto", 5, mockCookies, ); expect(results).toHaveLength(5); }); test("should return empty array for no results", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [], }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "nonexistent query", 1, "toronto", 25, mockCookies, ); expect(results).toEqual([]); }); test("should handle authentication errors gracefully", async () => { global.fetch = mock(() => Promise.resolve({ ok: false, status: 401, text: () => Promise.resolve("Unauthorized"), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "test", 1, "toronto", 25, mockCookies, ); expect(results).toEqual([]); }); test("should handle network errors", async () => { global.fetch = mock(() => Promise.reject(new Error("Network error"))); await expect( fetchFacebookItems("test", 1, "toronto", 25, mockCookies), ).rejects.toThrow("Network error"); }); test("should handle rate limiting with retry", async () => { let attempts = 0; global.fetch = mock(() => { attempts++; if (attempts === 1) { return Promise.resolve({ ok: false, status: 429, headers: { get: (header: string) => { if (header === "X-RateLimit-Reset") return "1"; return null; }, }, text: () => Promise.resolve("Rate limited"), }); } const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "Item 1", listing_price: { amount: "100.00", formatted_amount: "$100.00", currency: "CAD", }, is_live: true, }, }, }, ], }, }, }, }, }, }, ], ], }; return Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }); }); const results = await fetchFacebookItems( "test", 1, "toronto", 25, mockCookies, ); expect(attempts).toBe(2); expect(results).toHaveLength(1); }); }); describe("Vehicle Listing Integration", () => { const mockCookies = JSON.stringify([ { name: "c_user", value: "12345", domain: ".facebook.com", path: "/" }, { name: "xs", value: "abc123", domain: ".facebook.com", path: "/" }, ]); test("should correctly identify and parse vehicle listings", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "2006 Honda Civic", listing_price: { amount: "8000.00", formatted_amount: "$8,000.00", currency: "CAD", }, is_live: true, }, }, }, { node: { listing: { id: "2", marketplace_listing_title: "iPhone 13", listing_price: { amount: "800.00", formatted_amount: "$800.00", currency: "CAD", }, is_live: true, }, }, }, ], }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "cars", 1, "toronto", 25, mockCookies, ); expect(results).toHaveLength(2); // Both should be classified as "item" type in search results (vehicle detection is for item details) expect(results[0].title).toBe("2006 Honda Civic"); expect(results[1].title).toBe("iPhone 13"); }); }); describe("Different Categories", () => { const mockCookies = JSON.stringify([ { name: "c_user", value: "12345", domain: ".facebook.com", path: "/" }, { name: "xs", value: "abc123", domain: ".facebook.com", path: "/" }, ]); test("should handle electronics listings", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "Nintendo Switch", listing_price: { amount: "250.00", formatted_amount: "$250.00", currency: "CAD", }, location: { reverse_geocode: { city_page: { display_name: "Toronto" }, }, }, marketplace_listing_category_id: "479353692612078", condition: "USED", is_live: true, }, }, }, ], }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "nintendo switch", 1, "toronto", 25, mockCookies, ); expect(results).toHaveLength(1); expect(results[0].title).toBe("Nintendo Switch"); expect(results[0].categoryId).toBe("479353692612078"); }); test("should handle home goods/furniture listings", async () => { const mockSearchData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "Dining Table", listing_price: { amount: "150.00", formatted_amount: "$150.00", currency: "CAD", }, location: { reverse_geocode: { city_page: { display_name: "Mississauga" }, }, }, marketplace_listing_category_id: "1569171756675761", condition: "USED", is_live: true, }, }, }, ], }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "table", 1, "toronto", 25, mockCookies, ); expect(results).toHaveLength(1); expect(results[0].title).toBe("Dining Table"); expect(results[0].categoryId).toBe("1569171756675761"); }); }); describe("Error Scenarios", () => { const mockCookies = JSON.stringify([ { name: "c_user", value: "12345", domain: ".facebook.com", path: "/" }, { name: "xs", value: "abc123", domain: ".facebook.com", path: "/" }, ]); test("should handle malformed HTML responses", async () => { global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( "Invalid HTML without JSON data", ), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "test", 1, "toronto", 25, mockCookies, ); expect(results).toEqual([]); }); test("should handle 404 errors gracefully", async () => { global.fetch = mock(() => Promise.resolve({ ok: false, status: 404, text: () => Promise.resolve("Not found"), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "test", 1, "toronto", 25, mockCookies, ); expect(results).toEqual([]); }); test("should handle 500 errors gracefully", async () => { global.fetch = mock(() => Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve("Internal Server Error"), headers: { get: () => null, }, }), ); const results = await fetchFacebookItems( "test", 1, "toronto", 25, mockCookies, ); expect(results).toEqual([]); }); }); });