Ctrl K
ComunidadGuías…

test/shared/fetch.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import {
  apiErrorMessage,
  fetchText,
  jsonHeaders,
  parseApiError,
} from "#shared/fetch.ts";
import {
  getSubrequestUsage,
  runWithSubrequestBudget,
} from "#shared/subrequest-budget.ts";
import { stubFetch } from "#test-utils/fetch-stub.ts";

test("adds the JSON content type to authentication headers", () => {
  expect(jsonHeaders({ Authorization: "Bearer token" })).toEqual({
    Authorization: "Bearer token",
    "Content-Type": "application/json",
  });
});

describe("fetchText", () => {
  test("returns status, ok, text, and headers from a successful response", async () => {
    using _fetch = stubFetch(
      new Response('{"id":1}', {
        headers: { "Content-Type": "application/json" },
        status: 200,
      }),
    );

    const result = await fetchText("https://example.com/api");

    expect(result.status).toBe(200);
    expect(result.ok).toBe(true);
    expect(result.text).toBe('{"id":1}');
    expect(result.headers.get("Content-Type")).toBe("application/json");
  });

  test("returns ok false for error status codes", async () => {
    using _fetch = stubFetch(new Response("Not Found", { status: 404 }));

    const result = await fetchText("https://example.com/missing");

    expect(result.status).toBe(404);
    expect(result.ok).toBe(false);
    expect(result.text).toBe("Not Found");
  });

  test("handles empty response body", async () => {
    using _fetch = stubFetch(new Response(null, { status: 204 }));

    const result = await fetchText("https://example.com/empty", undefined, 0);

    expect(result.status).toBe(204);
    expect(result.ok).toBe(true);
    expect(result.text).toBe("");
  });

  test("accepts a response body at the byte limit", async () => {
    using _fetch = stubFetch(new Response("four"));

    const result = await fetchText("https://example.com/limited", undefined, 4);

    expect(result.text).toBe("four");
  });

  test("cancels a response body above the byte limit", async () => {
    let cancelled = false;
    const body = new ReadableStream<Uint8Array>({
      cancel: () => {
        cancelled = true;
      },
      start: (controller) => {
        controller.enqueue(new TextEncoder().encode("four"));
        controller.enqueue(new TextEncoder().encode("!"));
      },
    });
    using _fetch = stubFetch(new Response(body));

    await expect(
      fetchText("https://example.com/too-large", undefined, 4),
    ).rejects.toThrow("Response body exceeds 4 bytes");
    expect(cancelled).toBe(true);
  });

  test("forwards request init options", async () => {
    using fetchStub = stubFetch(new Response("ok"));

    await fetchText("https://example.com/post", {
      body: "payload",
      headers: { Authorization: "Bearer token" },
      method: "POST",
    });

    expect(fetchStub.calls.length).toBe(1);
    const [url, init] = fetchStub.calls[0]!.args;
    expect(url).toBe("https://example.com/post");
    expect(init?.method).toBe("POST");
    expect((init?.headers as Record<string, string>).Authorization).toBe(
      "Bearer token",
    );
    expect(init?.body).toBe("payload");
  });

  test("propagates fetch errors", async () => {
    using _fetch = stubFetch(new TypeError("Network error"));

    await expect(fetchText("https://example.com/fail")).rejects.toThrow(
      "Network error",
    );
  });

  test("counts one external subrequest", async () => {
    using _fetch = stubFetch(new Response("ok"));

    await runWithSubrequestBudget(async () => {
      await fetchText("https://example.com/api");
      expect(getSubrequestUsage()).toEqual({
        database: 0,
        external: 1,
        total: 1,
      });
    });
  });
});

describe("parseApiError", () => {
  test("extracts the 'message' field and reports ok:false", () => {
    const result = parseApiError(
      { status: 400, text: '{"message":"Bad input"}' },
      "Turso",
    );
    expect(result).toEqual({
      error: "Turso failed (400): Bad input",
      ok: false,
    });
  });

  test("falls back to the 'error' field when 'message' is absent", () => {
    const result = parseApiError(
      { status: 500, text: '{"error":"boom"}' },
      "Deploy",
    );
    expect(result).toEqual({ error: "Deploy failed (500): boom", ok: false });
  });

  test("prefers 'message' over 'error' (first matching key wins)", () => {
    const result = parseApiError(
      { status: 422, text: '{"message":"primary","error":"secondary"}' },
      "Bunny",
    );
    expect(result.error).toBe("Bunny failed (422): primary");
  });

  test("honours a custom key list", () => {
    const result = parseApiError(
      { status: 403, text: '{"detail":"nope","message":"ignored"}' },
      "Custom",
      ["detail"],
    );
    expect(result.error).toBe("Custom failed (403): nope");
  });

  test("uses the raw text when the body is not JSON", () => {
    const result = parseApiError(
      { status: 502, text: "Bad Gateway" },
      "Upstream",
    );
    expect(result.error).toBe("Upstream failed (502): Bad Gateway");
  });

  test("uses the raw text when JSON has no matching key", () => {
    const result = parseApiError(
      { status: 400, text: '{"unexpected":"shape"}' },
      "Api",
    );
    expect(result.error).toBe('Api failed (400): {"unexpected":"shape"}');
  });
});

describe("apiErrorMessage", () => {
  test("joins the messages of an errors array (SendGrid's shape)", () => {
    expect(
      apiErrorMessage(
        '{"errors":[{"message":"first","field":"from"},{"message":"second"}]}',
        ["message", "errors"],
      ),
    ).toBe("first; second");
  });

  test("accepts plain-string entries in an error array", () => {
    expect(apiErrorMessage('{"errors":["bad key"]}', ["errors"])).toBe(
      "bad key",
    );
  });

  test("skips array entries that carry no message", () => {
    expect(
      apiErrorMessage('{"errors":[null,{"code":9},{"message":"kept"}]}', [
        "errors",
      ]),
    ).toBe("kept");
  });

  test("returns the raw text when no array entry carries a message", () => {
    expect(apiErrorMessage('{"errors":[{"code":9}]}', ["errors"])).toBe(
      '{"errors":[{"code":9}]}',
    );
  });

  test("skips an empty-string message and tries the next key", () => {
    expect(apiErrorMessage('{"message":"","error":"boom"}')).toBe("boom");
  });

  test("ignores a non-string message value", () => {
    expect(apiErrorMessage('{"message":{"nested":true}}')).toBe(
      '{"message":{"nested":true}}',
    );
  });

  test("returns the raw text for a non-object JSON body", () => {
    expect(apiErrorMessage("null")).toBe("null");
  });
});