fix: treat empty DataForSEO backlinks results as valid empty data (#109)
This commit is contained in:
parent
53505c0a5b
commit
fb1291537c
@ -10,6 +10,8 @@ vi.mock("@/server/lib/dataforseoBacklinksAccount", () => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
fetchBacklinksHistoryRaw,
|
||||
fetchBacklinksRowsRaw,
|
||||
fetchBacklinksSummaryRaw,
|
||||
normalizeBacklinksTarget,
|
||||
} from "@/server/lib/dataforseoBacklinks";
|
||||
@ -145,7 +147,7 @@ describe("fetchBacklinksSummaryRaw", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("treats null summary results as validation errors", async () => {
|
||||
it("treats null summary results as a valid zero-data response", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@ -168,7 +170,83 @@ describe("fetchBacklinksSummaryRaw", () => {
|
||||
fetchBacklinksSummaryRaw({
|
||||
target: "not-a-real-input.example",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "VALIDATION_ERROR" });
|
||||
).resolves.toMatchObject({ data: {} });
|
||||
});
|
||||
|
||||
it("treats empty summary results as a valid zero-data response", async () => {
|
||||
vi.mocked(fetch).mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
status_code: 20000,
|
||||
status_message: "Ok.",
|
||||
tasks: [
|
||||
{
|
||||
status_code: 20000,
|
||||
status_message: "Ok.",
|
||||
result: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
);
|
||||
vi.mocked(classifyBacklinksErrorWithAccountState).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
fetchBacklinksSummaryRaw({
|
||||
target: "example.com",
|
||||
}),
|
||||
).resolves.toMatchObject({ data: {} });
|
||||
});
|
||||
|
||||
it("treats empty backlinks rows and history results as valid empty arrays", async () => {
|
||||
vi.mocked(fetch)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
status_code: 20000,
|
||||
status_message: "Ok.",
|
||||
tasks: [
|
||||
{
|
||||
status_code: 20000,
|
||||
status_message: "Ok.",
|
||||
result: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
status_code: 20000,
|
||||
status_message: "Ok.",
|
||||
tasks: [
|
||||
{
|
||||
status_code: 20000,
|
||||
status_message: "Ok.",
|
||||
result: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
);
|
||||
vi.mocked(classifyBacklinksErrorWithAccountState).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
fetchBacklinksRowsRaw({
|
||||
target: "example.com",
|
||||
}),
|
||||
).resolves.toMatchObject({ data: [] });
|
||||
await expect(
|
||||
fetchBacklinksHistoryRaw({
|
||||
target: "example.com",
|
||||
dateFrom: "2025-01-01",
|
||||
dateTo: "2025-12-31",
|
||||
}),
|
||||
).resolves.toMatchObject({ data: [] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
backlinksItemSchema,
|
||||
backlinksSummaryItemSchema,
|
||||
domainPageSummaryItemSchema,
|
||||
parseFirstResult,
|
||||
parseItems,
|
||||
referringDomainItemSchema,
|
||||
responseSchema,
|
||||
@ -179,11 +178,21 @@ export async function fetchBacklinksSummaryRaw(input: BacklinksRequest) {
|
||||
const response = await postBacklinks("/v3/backlinks/summary/live", [
|
||||
buildCommonPayload(input),
|
||||
]);
|
||||
const data = parseFirstResult(
|
||||
"backlinks-summary-live",
|
||||
response.results,
|
||||
backlinksSummaryItemSchema,
|
||||
const firstResult = response.results[0];
|
||||
const parsed = firstResult
|
||||
? backlinksSummaryItemSchema.safeParse(firstResult)
|
||||
: null;
|
||||
if (parsed && !parsed.success) {
|
||||
console.error(
|
||||
"dataforseo.backlinks-summary-live.invalid-result",
|
||||
parsed.error.issues.slice(0, 5),
|
||||
);
|
||||
throw new AppError(
|
||||
"INTERNAL_ERROR",
|
||||
"DataForSEO backlinks-summary-live returned an invalid response shape",
|
||||
);
|
||||
}
|
||||
const data = parsed?.data ?? {};
|
||||
return {
|
||||
data,
|
||||
billing: response.billing,
|
||||
|
||||
@ -210,8 +210,7 @@ export function parseItems<T extends z.ZodTypeAny>(
|
||||
): Array<z.infer<T>> {
|
||||
const firstResult = results[0] ?? null;
|
||||
if (firstResult == null) {
|
||||
console.warn(`dataforseo.${endpointName}.empty-result`);
|
||||
throw new AppError("VALIDATION_ERROR", "Backlinks target is invalid");
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsed = z.array(itemSchema).safeParse(firstResult.items ?? []);
|
||||
@ -228,29 +227,3 @@ export function parseItems<T extends z.ZodTypeAny>(
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function parseFirstResult<T extends z.ZodTypeAny>(
|
||||
endpointName: string,
|
||||
results: BacklinksTaskResult[],
|
||||
resultSchema: T,
|
||||
): z.infer<T> {
|
||||
const firstResult = results[0] ?? null;
|
||||
if (firstResult == null) {
|
||||
console.warn(`dataforseo.${endpointName}.empty-result`);
|
||||
throw new AppError("VALIDATION_ERROR", "Backlinks target is invalid");
|
||||
}
|
||||
|
||||
const parsed = resultSchema.safeParse(firstResult);
|
||||
if (!parsed.success) {
|
||||
console.error(
|
||||
`dataforseo.${endpointName}.invalid-result`,
|
||||
parsed.error.issues.slice(0, 5),
|
||||
);
|
||||
throw new AppError(
|
||||
"INTERNAL_ERROR",
|
||||
`DataForSEO ${endpointName} returned an invalid response shape`,
|
||||
);
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user