diff --git a/test/facebook-core.test.ts b/test/facebook-core.test.ts new file mode 100644 index 0000000..ea3b5a1 --- /dev/null +++ b/test/facebook-core.test.ts @@ -0,0 +1,748 @@ +import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"; +import { + fetchFacebookItem, + extractFacebookItemData, + extractFacebookMarketplaceData, + parseFacebookItem, + parseFacebookAds, + formatCentsToCurrency, + loadFacebookCookies, + formatCookiesForHeader, + parseFacebookCookieString, +} 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(""); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/facebook-integration.test.ts b/test/facebook-integration.test.ts new file mode 100644 index 0000000..b89ba65 --- /dev/null +++ b/test/facebook-integration.test.ts @@ -0,0 +1,517 @@ +import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"; +import fetchFacebookItems, { fetchFacebookItem } from "../src/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([]); + }); + }); +});