import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { extractFacebookItemData, extractFacebookMarketplaceData, fetchFacebookItem, formatCentsToCurrency, formatCookiesForHeader, loadFacebookCookies, parseFacebookAds, parseFacebookCookieString, parseFacebookItem, } from "../src/facebook"; // Mock fetch globally const originalFetch = global.fetch; describe("Facebook Marketplace Scraper Core Tests", () => { beforeEach(() => { global.fetch = mock(() => { throw new Error("fetch should be mocked in individual tests"); }); }); afterEach(() => { global.fetch = originalFetch; }); describe("Cookie Parsing", () => { describe("parseFacebookCookieString", () => { test("should parse valid cookie string", () => { const cookieString = "c_user=123456789; xs=abcdef123456; fr=xyz789"; const result = parseFacebookCookieString(cookieString); expect(result).toHaveLength(3); expect(result[0]).toEqual({ name: "c_user", value: "123456789", domain: ".facebook.com", path: "/", secure: true, httpOnly: false, sameSite: "lax", expirationDate: undefined, }); expect(result[1]).toEqual({ name: "xs", value: "abcdef123456", domain: ".facebook.com", path: "/", secure: true, httpOnly: false, sameSite: "lax", expirationDate: undefined, }); }); test("should handle URL-encoded values", () => { const cookieString = "c_user=123%2B456; xs=abc%3Ddef"; const result = parseFacebookCookieString(cookieString); expect(result[0].value).toBe("123+456"); expect(result[1].value).toBe("abc=def"); }); test("should filter out malformed cookies", () => { const cookieString = "c_user=123; invalid; xs=abc; =empty"; const result = parseFacebookCookieString(cookieString); expect(result).toHaveLength(2); expect(result.map((c) => c.name)).toEqual(["c_user", "xs"]); }); test("should handle empty input", () => { expect(parseFacebookCookieString("")).toEqual([]); expect(parseFacebookCookieString(" ")).toEqual([]); }); test("should handle extra whitespace", () => { const cookieString = " c_user = 123 ; xs=abc "; const result = parseFacebookCookieString(cookieString); expect(result).toHaveLength(2); expect(result[0].name).toBe("c_user"); expect(result[0].value).toBe("123"); expect(result[1].name).toBe("xs"); expect(result[1].value).toBe("abc"); }); }); }); describe("Facebook Item Fetching", () => { describe("fetchFacebookItem", () => { const mockCookies = JSON.stringify([ { name: "c_user", value: "12345", domain: ".facebook.com" }, { name: "xs", value: "abc123", domain: ".facebook.com" }, ]); test("should handle authentication errors", async () => { global.fetch = mock(() => Promise.resolve({ ok: false, status: 401, text: () => Promise.resolve("Authentication required"), headers: { get: () => null, }, }), ); const result = await fetchFacebookItem("123", mockCookies); expect(result).toBeNull(); }); test("should handle item not found", async () => { global.fetch = mock(() => Promise.resolve({ ok: false, status: 404, text: () => Promise.resolve("Not found"), headers: { get: () => null, }, }), ); const result = await fetchFacebookItem("nonexistent", mockCookies); expect(result).toBeNull(); }); test("should handle rate limiting", 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 mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { viewer: { marketplace_product_details_page: { target: { id: "123", __typename: "GroupCommerceProductItem", marketplace_listing_title: "Test Item", is_live: true, }, }, }, }, }, }, }, ], ], }; return Promise.resolve({ ok: true, text: () => Promise.resolve( `
`, ), headers: { get: () => null, }, }); }); const result = await fetchFacebookItem("123", mockCookies); expect(attempts).toBe(2); // Should eventually succeed after retry }); test("should handle sold items", async () => { const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { viewer: { marketplace_product_details_page: { target: { id: "456", __typename: "GroupCommerceProductItem", marketplace_listing_title: "Sold Item", is_sold: true, is_live: false, }, }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const result = await fetchFacebookItem("456", mockCookies); expect(result?.listingStatus).toBe("SOLD"); }); test("should handle missing authentication cookies", async () => { // Use a test-specific cookie file that doesn't exist const testCookiePath = "./cookies/facebook-test.json"; // Test with no cookies available (test file doesn't exist) await expect( fetchFacebookItem("123", undefined, testCookiePath), ).rejects.toThrow("No valid Facebook cookies found"); }); test("should handle successful item extraction", async () => { const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { viewer: { marketplace_product_details_page: { target: { id: "789", __typename: "GroupCommerceProductItem", marketplace_listing_title: "Working Item", formatted_price: { text: "$299.00" }, listing_price: { amount: "299.00", currency: "CAD", }, is_live: true, creation_time: 1640995200, }, }, }, }, }, }, }, ], ], }; global.fetch = mock(() => Promise.resolve({ ok: true, text: () => Promise.resolve( ``, ), headers: { get: () => null, }, }), ); const result = await fetchFacebookItem("789", mockCookies); expect(result).not.toBeNull(); expect(result?.title).toBe("Working Item"); expect(result?.listingPrice?.amountFormatted).toBe("$299.00"); expect(result?.listingStatus).toBe("ACTIVE"); }); test("should handle server errors", async () => { global.fetch = mock(() => Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve("Internal Server Error"), headers: { get: () => null, }, }), ); const result = await fetchFacebookItem("error", mockCookies); expect(result).toBeNull(); }); }); }); describe("Data Extraction", () => { describe("extractFacebookItemData", () => { test("should extract item data from standard require structure", () => { const mockItemData = { id: "123456", __typename: "GroupCommerceProductItem", marketplace_listing_title: "Test Item", formatted_price: { text: "$100.00" }, listing_price: { amount: "100.00", currency: "CAD" }, is_live: true, }; const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { viewer: { marketplace_product_details_page: { target: mockItemData, }, }, }, }, }, }, ], ], }; const html = ``; const result = extractFacebookItemData(html); expect(result).not.toBeNull(); expect(result?.id).toBe("123456"); expect(result?.marketplace_listing_title).toBe("Test Item"); }); test("should handle missing item data", () => { const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { viewer: { marketplace_product_details_page: {}, }, }, }, }, }, ], ], }; const html = ``; const result = extractFacebookItemData(html); expect(result).toBeNull(); }); test("should handle malformed HTML", () => { const result = extractFacebookItemData( "Invalid HTML", ); expect(result).toBeNull(); }); test("should handle invalid JSON in script tags", () => { const html = ""; const result = extractFacebookItemData(html); expect(result).toBeNull(); }); test("should extract item with vehicle data", () => { const mockVehicleItem = { id: "789", __typename: "GroupCommerceProductItem", marketplace_listing_title: "2006 Honda Civic", formatted_price: { text: "$5,000" }, listing_price: { amount: "5000.00", currency: "CAD" }, vehicle_make_display_name: "Honda", vehicle_model_display_name: "Civic", vehicle_odometer_data: { unit: "KILOMETERS", value: 150000 }, vehicle_transmission_type: "AUTOMATIC", is_live: true, }; const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { viewer: { marketplace_product_details_page: { target: mockVehicleItem, }, }, }, }, }, }, ], ], }; const html = ``; const result = extractFacebookItemData(html); expect(result).not.toBeNull(); expect(result?.vehicle_make_display_name).toBe("Honda"); expect(result?.vehicle_odometer_data?.value).toBe(150000); }); }); describe("extractFacebookMarketplaceData", () => { test("should extract search results from marketplace data", () => { const mockMarketplaceData = { feed_units: { edges: [ { node: { listing: { id: "1", marketplace_listing_title: "Item 1", listing_price: { amount: "10.00", currency: "CAD" }, }, }, }, { node: { listing: { id: "2", marketplace_listing_title: "Item 2", listing_price: { amount: "20.00", currency: "CAD" }, }, }, }, ], }, }; const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: mockMarketplaceData, }, }, }, }, ], ], }; const html = ``; const result = extractFacebookMarketplaceData(html); expect(result).not.toBeNull(); expect(result).toHaveLength(2); expect(result?.[0].node.listing.marketplace_listing_title).toBe( "Item 1", ); }); test("should handle empty search results", () => { const mockData = { require: [ [ null, null, null, { __bbox: { result: { data: { marketplace_search: { feed_units: { edges: [] }, }, }, }, }, }, ], ], }; const html = ``; const result = extractFacebookMarketplaceData(html); expect(result).toBeNull(); }); }); }); describe("Data Parsing", () => { describe("parseFacebookItem", () => { test("should parse complete item with all fields", () => { const item = { id: "123456", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "iPhone 13 Pro", redacted_description: { text: "Excellent condition" }, formatted_price: { text: "$800.00" }, listing_price: { amount: "800.00", currency: "CAD" }, location_text: { text: "Toronto, ON" }, is_live: true, creation_time: 1640995200, marketplace_listing_seller: { id: "seller1", name: "John Doe", }, delivery_types: ["IN_PERSON"], }; const result = parseFacebookItem(item); expect(result).not.toBeNull(); expect(result?.title).toBe("iPhone 13 Pro"); expect(result?.description).toBe("Excellent condition"); expect(result?.listingPrice?.amountFormatted).toBe("$800.00"); expect(result?.listingPrice?.cents).toBe(80000); expect(result?.listingPrice?.currency).toBe("CAD"); expect(result?.address).toBe("Toronto, ON"); expect(result?.listingStatus).toBe("ACTIVE"); expect(result?.seller?.name).toBe("John Doe"); expect(result?.deliveryTypes).toEqual(["IN_PERSON"]); }); test("should parse FREE items", () => { const item = { id: "789", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "Free Sofa", formatted_price: { text: "FREE" }, listing_price: { amount: "0.00", currency: "CAD" }, is_live: true, }; const result = parseFacebookItem(item); expect(result).not.toBeNull(); expect(result?.title).toBe("Free Sofa"); expect(result?.listingPrice?.amountFormatted).toBe("FREE"); expect(result?.listingPrice?.cents).toBe(0); }); test("should handle missing optional fields", () => { const item = { id: "456", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "Minimal Item", }; const result = parseFacebookItem(item); expect(result).not.toBeNull(); expect(result?.title).toBe("Minimal Item"); expect(result?.description).toBeUndefined(); expect(result?.seller).toBeUndefined(); }); test("should identify vehicle listings", () => { const vehicleItem = { id: "999", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "2012 Mazda 3", formatted_price: { text: "$8,000" }, listing_price: { amount: "8000.00", currency: "CAD" }, vehicle_make_display_name: "Mazda", vehicle_model_display_name: "3", is_live: true, }; const result = parseFacebookItem(vehicleItem); expect(result?.listingType).toBe("vehicle"); }); test("should handle different listing statuses", () => { const soldItem = { id: "111", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "Sold Item", is_sold: true, is_live: false, }; const pendingItem = { id: "222", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "Pending Item", is_pending: true, is_live: true, }; const hiddenItem = { id: "333", __typename: "GroupCommerceProductItem" as const, marketplace_listing_title: "Hidden Item", is_hidden: true, is_live: false, }; expect(parseFacebookItem(soldItem)?.listingStatus).toBe("SOLD"); expect(parseFacebookItem(pendingItem)?.listingStatus).toBe("PENDING"); expect(parseFacebookItem(hiddenItem)?.listingStatus).toBe("HIDDEN"); }); test("should return null for items without title", () => { const invalidItem = { id: "invalid", __typename: "GroupCommerceProductItem" as const, is_live: true, }; const result = parseFacebookItem(invalidItem); expect(result).toBeNull(); }); }); describe("parseFacebookAds", () => { test("should parse search result ads", () => { const ads = [ { node: { listing: { id: "1", marketplace_listing_title: "Ad 1", listing_price: { amount: "50.00", formatted_amount: "$50.00", currency: "CAD", }, location: { reverse_geocode: { city_page: { display_name: "Toronto" } }, }, creation_time: 1640995200, is_live: true, }, }, }, { node: { listing: { id: "2", marketplace_listing_title: "Ad 2", listing_price: { amount: "75.00", formatted_amount: "$75.00", currency: "CAD", }, location: { reverse_geocode: { city_page: { display_name: "Ottawa" } }, }, creation_time: 1640995300, is_live: true, }, }, }, ]; const results = parseFacebookAds(ads); expect(results).toHaveLength(2); expect(results[0].title).toBe("Ad 1"); expect(results[0].listingPrice?.cents).toBe(5000); expect(results[0].address).toBe("Toronto"); expect(results[1].title).toBe("Ad 2"); expect(results[1].address).toBe("Ottawa"); }); test("should filter out ads without price", () => { const ads = [ { 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, }, }, }, ]; const results = parseFacebookAds(ads); expect(results).toHaveLength(1); expect(results[0].title).toBe("With Price"); }); test("should handle malformed ads gracefully", () => { const ads = [ { node: { listing: { id: "1", marketplace_listing_title: "Valid Ad", listing_price: { amount: "50.00", formatted_amount: "$50.00", currency: "CAD", }, is_live: true, }, }, }, { node: { // Missing listing }, } as { node: { listing?: unknown } }, ]; const results = parseFacebookAds(ads); expect(results).toHaveLength(1); expect(results[0].title).toBe("Valid Ad"); }); }); }); describe("Utility Functions", () => { describe("formatCentsToCurrency", () => { test("should format cents to currency string", () => { expect(formatCentsToCurrency(100)).toBe("$1.00"); expect(formatCentsToCurrency(1000)).toBe("$10.00"); expect(formatCentsToCurrency(9999)).toBe("$99.99"); expect(formatCentsToCurrency(123456)).toBe("$1,234.56"); }); test("should handle string inputs", () => { expect(formatCentsToCurrency("100")).toBe("$1.00"); expect(formatCentsToCurrency("1000")).toBe("$10.00"); }); test("should handle zero", () => { expect(formatCentsToCurrency(0)).toBe("$0.00"); }); test("should handle null and undefined", () => { expect(formatCentsToCurrency(null)).toBe(""); expect(formatCentsToCurrency(undefined)).toBe(""); }); test("should handle invalid inputs", () => { expect(formatCentsToCurrency("invalid")).toBe(""); expect(formatCentsToCurrency(Number.NaN)).toBe(""); }); }); describe("formatCookiesForHeader", () => { const mockCookies = [ { name: "c_user", value: "123456", domain: ".facebook.com", path: "/" }, { name: "xs", value: "abcdef", domain: ".facebook.com", path: "/" }, { name: "session_id", value: "xyz", domain: "other.com", path: "/" }, ]; test("should format cookies for header string", () => { const result = formatCookiesForHeader(mockCookies, "www.facebook.com"); expect(result).toBe("c_user=123456; xs=abcdef"); }); test("should filter expired cookies", () => { const cookiesWithExpiration = [ ...mockCookies, { name: "expired", value: "old", domain: ".facebook.com", path: "/", expirationDate: Date.now() / 1000 - 1000, }, ]; const result = formatCookiesForHeader( cookiesWithExpiration, "www.facebook.com", ); expect(result).not.toContain("expired"); }); test("should handle no matching cookies", () => { const result = formatCookiesForHeader(mockCookies, "www.google.com"); expect(result).toBe(""); }); test("should handle empty cookie array", () => { const result = formatCookiesForHeader([], "www.facebook.com"); expect(result).toBe(""); }); }); }); });