diff --git a/src/server/lib/dataforseo/serp.test.ts b/src/server/lib/dataforseo/serp.test.ts index f0073ce..c0e3da4 100644 --- a/src/server/lib/dataforseo/serp.test.ts +++ b/src/server/lib/dataforseo/serp.test.ts @@ -5,6 +5,7 @@ vi.mock("@/server/lib/runtime-env", () => ({ })); import { + fetchLiveSerp, fetchRankCheckTaskResult, postRankCheckTasks, } from "@/server/lib/dataforseo/serp"; @@ -17,6 +18,43 @@ function parseDataforseoRequestBody(init: RequestInit | undefined): unknown { return JSON.parse(body) as unknown; } +describe("live SERP", () => { + // 40102 is the documented "No Search Results." code (40501 is "Invalid + // Field."). isNoResultsTask matches on the status message, not the code, so + // this stays correct whichever code DataForSEO attaches to the message. + it("returns an empty result for DataForSEO's no-results task", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + Response.json({ + status_code: 20000, + tasks: [ + { + status_code: 40102, + status_message: "No Search Results.", + path: ["v3", "serp", "google", "organic", "live", "advanced"], + cost: 0.002, + result_count: 0, + result: [], + }, + ], + }), + ), + ); + + await expect( + fetchLiveSerp({ + keyword: "obscure query", + locationCode: 2840, + languageCode: "en", + }), + ).resolves.toMatchObject({ + data: [], + billing: { costUsd: 0.002 }, + }); + }); +}); + describe("rank check task queue", () => { beforeEach(() => { vi.restoreAllMocks(); diff --git a/src/server/lib/dataforseo/serp.ts b/src/server/lib/dataforseo/serp.ts index a89c146..50b788c 100644 --- a/src/server/lib/dataforseo/serp.ts +++ b/src/server/lib/dataforseo/serp.ts @@ -92,7 +92,9 @@ export async function fetchLiveSerp(input: { }, ], ); - const task = assertOk(response); + // DataForSEO uses a task error for a valid empty SERP. Keep the charged + // response in the normal billing path and return an empty item list. + const task = assertOk(response, { treatNoResultsAsEmpty: true }); return { data: parseTaskItems( "google-organic-live-advanced",