Files
ca-marketplace-scraper/test/facebook-integration.test.ts

518 lines
13 KiB
TypeScript

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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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(`<html><body><script>${JSON.stringify(mockSearchData)}</script></body></html>`),
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("<html><body>Invalid HTML without JSON data</body></html>"),
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([]);
});
});
});