metatron-open-seo/src/server/lib/dataforseoSchemas.test.ts
Ben Senescu f3ef909d3e fix: handle empty DataForSEO task results (#84)
* fix: accept empty DataForSEO task results

Treat successful tasks with null items as empty payloads so empty ranked keyword responses do not fail billing validation.

* refactor: simplify DataForSEO null result handling

Add .nullable() to the existing structured result schema instead of
loosening the type to unknown[] and re-parsing in parseTaskItems.

* refactor: simplify backlinks zod parsing with structured result schema

Same pattern as the dataforseoSchemas fix: give taskSchema.result a
structured type with .items instead of z.unknown(), removing the
intermediate resultItemsSchema and two-step parsing in parseItems.

* refactor: replace manual type guards with zod schemas

- progress-kv.ts: replace isCrawledUrlEntry type guard with a zod
  schema and use jsonCodec(z.array(...)) instead of parsing unknown
  then filtering
- helpers.ts: tighten normalizeIntent param from unknown to
  string | null | undefined to match actual call sites
- dataforseoBacklinksSupport.ts: allow null result elements to match
  API responses where result contains [null]

* refactor: filter null result elements at the source

Filter out null elements from task.result in postBacklinks so
downstream functions receive clean BacklinksTaskResult[] instead
of (BacklinksTaskResult | null)[].

* save
2026-04-08 14:09:02 -04:00

76 lines
1.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
domainRankedKeywordItemSchema,
parseTaskItems,
relatedKeywordItemSchema,
successfulDataforseoTaskSchema,
} from "@/server/lib/dataforseoSchemas";
describe("dataforseoSchemas", () => {
it("accepts null items for empty successful tasks", () => {
const task = {
id: "04042314-1577-0387-0000-33dc4b485cfd",
status_code: 20000,
status_message: "Ok.",
path: ["v3", "dataforseo_labs", "google", "related_keywords", "live"],
cost: 0.02,
result_count: 1,
result: [
{
se_type: "google",
seed_keyword: "canva ai video alternative",
location_code: 2840,
language_code: "en",
total_count: null,
items_count: 0,
items: null,
},
],
};
const parsedTask = successfulDataforseoTaskSchema.parse(task);
expect(
parseTaskItems(
"google-related-keywords-live",
parsedTask,
relatedKeywordItemSchema,
),
).toEqual([]);
});
it("accepts empty ranked keyword tasks with null items", () => {
const task = {
id: "04070246-1577-0381-0000-2c56c059f67e",
status_code: 20000,
status_message: "Ok.",
path: ["v3", "dataforseo_labs", "google", "ranked_keywords", "live"],
cost: 0.01,
result_count: 1,
result: [
{
se_type: "google",
target: "openseo.so",
location_code: 2840,
language_code: "en",
total_count: null,
items_count: 0,
metrics: null,
metrics_absolute: null,
items: null,
},
],
};
const parsedTask = successfulDataforseoTaskSchema.parse(task);
expect(
parseTaskItems(
"google-ranked-keywords-live",
parsedTask,
domainRankedKeywordItemSchema,
),
).toEqual([]);
});
});