Ctrl K
ComunidadGuías…

test/shared/bunny-dns.test.ts

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

Archivo original ↗Preguntar a KiBot
import { expect } from "@std/expect";
import { it as test } from "@std/testing/bdd";
import { stub } from "@std/testing/mock";
import { bunnyCdnApi } from "#shared/bunny-cdn.ts";
import { buildSubdomainRecordName } from "#shared/bunny-dns.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { setupErrorSpy } from "#test-utils/error-spy.ts";
import { stubFetch } from "#test-utils/fetch-stub.ts";
import { restoreStubsAfterEach } from "#test-utils/mocks.ts";

const ENV = {
  BUNNY_API_KEY: "test-key",
  BUNNY_DNS_SUBDOMAIN_SUFFIX: ".tickets",
  BUNNY_DNS_ZONE_ID: "7",
  BUNNY_SCRIPT_ID: "42",
};

const zone = (
  records: { Id: number; Name: string; Type: number; Value: string }[] = [],
) => ({
  Domain: "example.test",
  Id: 7,
  Records: records,
});

type Restorable = { restore(): void };

const stubPullZoneId = (
  stubs: Restorable[],
  result: { id: number; ok: true } | { error: string; ok: false } = {
    id: 9,
    ok: true,
  },
): void => {
  stubs.push(
    stub(bunnyCdnApi, "findPullZoneId", () => Promise.resolve(result)),
  );
};

const stubAvailableSubdomain = (
  stubs: Restorable[],
  target: { hostname: string; ok: true } | { error: string; ok: false } = {
    hostname: "child.b-cdn.net",
    ok: true,
  },
): void => {
  stubs.push(
    stub(bunnyCdnApi, "checkSubdomainAvailable", () =>
      Promise.resolve({
        available: true,
        fullDomain: "child.tickets.example.test",
        ok: true,
      }),
    ),
    stub(bunnyCdnApi, "getCdnHostname", () => Promise.resolve(target)),
  );
};

const noContent = (): Response => new Response(null, { status: 204 });

const stubCustomDomainRequests = (
  first = noContent(),
): ReturnType<typeof stubFetch> => stubFetch(first, noContent(), noContent());

describeWithEnv("Bunny domain API", { env: ENV }, () => {
  const stubs: Restorable[] = [];
  restoreStubsAfterEach(stubs);
  const errors = setupErrorSpy();

  test("loads a DNS zone with the configured credential", async () => {
    using fetchStub = stubFetch(new Response(JSON.stringify(zone())));

    expect(await bunnyCdnApi.getDnsZone()).toEqual({ ok: true, zone: zone() });
    const [url, init] = fetchStub.calls[0]!.args as [string, RequestInit];
    expect(url).toBe("https://api.bunny.net/dnszone/7");
    expect(init.headers).toEqual({ AccessKey: "test-key" });
    expect(init.method).toBeUndefined();
  });

  test("labels a DNS zone provider failure", async () => {
    using _fetch = stubFetch(new Response("failed", { status: 500 }));

    expect(await bunnyCdnApi.getDnsZone()).toEqual({
      error: "Get DNS zone failed (500): failed",
      ok: false,
    });
  });

  test("builds and checks the full subdomain name", async () => {
    stubs.push(
      stub(bunnyCdnApi, "getDnsZone", () =>
        Promise.resolve({
          ok: true,
          zone: zone([
            { Id: 1, Name: "used.tickets", Type: 2, Value: "target" },
          ]),
        }),
      ),
    );

    expect(buildSubdomainRecordName("used")).toBe("used.tickets");
    expect(await bunnyCdnApi.checkSubdomainAvailable("used")).toEqual({
      available: false,
      fullDomain: "used.tickets.example.test",
      ok: true,
    });
    expect(await bunnyCdnApi.checkSubdomainAvailable("free")).toEqual({
      available: true,
      fullDomain: "free.tickets.example.test",
      ok: true,
    });
  });

  test("returns a DNS lookup failure before checking records", async () => {
    stubs.push(
      stub(bunnyCdnApi, "getDnsZone", () =>
        Promise.resolve({ error: "zone failed", ok: false }),
      ),
    );
    expect(await bunnyCdnApi.checkSubdomainAvailable("free")).toEqual({
      error: "zone failed",
      ok: false,
    });
  });

  test("registers a custom hostname and enables forced SSL", async () => {
    stubPullZoneId(stubs);
    using fetchStub = stubCustomDomainRequests();

    expect(
      await bunnyCdnApi.validateCustomDomain("custom.example.test"),
    ).toEqual({ ok: true });
    const calls = fetchStub.calls.map(
      ({ args }) => args as [string, RequestInit],
    );
    expect(calls.map(([url]) => url)).toEqual([
      "https://api.bunny.net/pullzone/9/addHostname",
      "https://api.bunny.net/pullzone/loadFreeCertificate?hostname=custom.example.test",
      "https://api.bunny.net/pullzone/9/setForceSSL",
    ]);
    expect(calls.map(([, init]) => init.method)).toEqual([
      "POST",
      "GET",
      "POST",
    ]);
    expect(JSON.parse(calls[0]![1].body as string)).toEqual({
      Hostname: "custom.example.test",
    });
    expect(JSON.parse(calls[2]![1].body as string)).toEqual({
      ForceSSL: true,
      Hostname: "custom.example.test",
    });
    expect(new Headers(calls[0]![1].headers).get("content-type")).toBe(
      "application/json",
    );
  });

  test("continues when the custom hostname is already registered", async () => {
    stubPullZoneId(stubs);
    using _fetch = stubCustomDomainRequests(
      new Response(
        JSON.stringify({
          ErrorKey: "pullzone.hostname_already_registered",
          Message: "already registered",
        }),
        { status: 400 },
      ),
    );

    expect(
      await bunnyCdnApi.validateCustomDomain("custom.example.test"),
    ).toEqual({ ok: true });
  });

  test("reports a pull-zone lookup failure", async () => {
    stubPullZoneId(stubs, { error: "zone failed", ok: false });

    expect(
      await bunnyCdnApi.validateCustomDomain("custom.example.test"),
    ).toEqual({ error: "zone failed", ok: false });
    expect(errors.contains("zone failed")).toBe(true);
  });

  for (const [label, responses, expected] of [
    [
      "hostname",
      [new Response("add failed", { status: 500 })],
      "Add hostname failed (500): add failed",
    ],
    [
      "certificate",
      [
        new Response(null, { status: 204 }),
        new Response("cert failed", { status: 500 }),
      ],
      "Load free certificate failed (500): cert failed",
    ],
    [
      "forced SSL",
      [
        new Response(null, { status: 204 }),
        new Response(null, { status: 204 }),
        new Response("ssl failed", { status: 500 }),
      ],
      "Set force SSL failed (500): ssl failed",
    ],
  ] as const) {
    test(`reports a ${label} setup failure`, async () => {
      stubPullZoneId(stubs);
      using _fetch = stubFetch(responses[0], ...responses.slice(1));

      const result = await bunnyCdnApi.validateCustomDomain(
        "custom.example.test",
      );

      expect(result).toMatchObject({ error: expected, ok: false });
      expect(errors.contains(expected)).toBe(true);
    });
  }

  test("registers a CNAME with the exact DNS payload", async () => {
    stubAvailableSubdomain(stubs);
    stubs.push(
      stub(bunnyCdnApi, "validateCustomDomain", () =>
        Promise.resolve({ ok: true }),
      ),
    );
    using fetchStub = stubFetch(new Response(JSON.stringify({ Id: 12 })));

    expect(await bunnyCdnApi.registerBunnySubdomain("child")).toEqual({
      fullDomain: "child.tickets.example.test",
      ok: true,
    });
    const [url, init] = fetchStub.calls[0]!.args as [string, RequestInit];
    expect(url).toBe("https://api.bunny.net/dnszone/7/records");
    expect(init.method).toBe("PUT");
    expect(JSON.parse(init.body as string)).toEqual({
      Name: "child.tickets",
      Ttl: 300,
      Type: 2,
      Value: "child.b-cdn.net",
    });
  });

  test("rejects a taken subdomain before provider writes", async () => {
    stubs.push(
      stub(bunnyCdnApi, "checkSubdomainAvailable", () =>
        Promise.resolve({
          available: false,
          fullDomain: "used.tickets.example.test",
          ok: true,
        }),
      ),
    );
    using fetchStub = stubFetch(() => {
      throw new Error("Unexpected provider request");
    });

    expect(await bunnyCdnApi.registerBunnySubdomain("used")).toEqual({
      error: 'Subdomain "used" is already taken',
      ok: false,
    });
    expect(fetchStub.calls.length).toBe(0);
  });

  test("reports a CDN target lookup failure", async () => {
    stubAvailableSubdomain(stubs, { error: "target failed", ok: false });

    expect(await bunnyCdnApi.registerBunnySubdomain("child")).toEqual({
      error: "target failed",
      ok: false,
    });
    expect(errors.contains("target failed")).toBe(true);
  });

  test("reports a DNS record write failure", async () => {
    stubAvailableSubdomain(stubs);
    using _fetch = stubFetch(new Response("write failed", { status: 500 }));

    expect(await bunnyCdnApi.registerBunnySubdomain("child")).toEqual({
      error: "Add DNS CNAME record failed (500): write failed",
      ok: false,
    });
    expect(errors.contains("Add DNS CNAME record failed")).toBe(true);
  });

  test("retries certificate setup with bounded backoff and cleans up", async () => {
    const delays: number[] = [];
    const validations: string[] = [];
    const deleted: [string, number][] = [];
    stubAvailableSubdomain(stubs);
    stubs.push(
      stub(bunnyCdnApi, "validateCustomDomain", (hostname) => {
        validations.push(hostname);
        return Promise.resolve({ error: "certificate pending", ok: false });
      }),
      stub(bunnyCdnApi, "delay", (milliseconds) => {
        delays.push(milliseconds);
        return Promise.resolve();
      }),
      stub(bunnyCdnApi, "deleteDnsRecord", (zoneId, recordId) => {
        deleted.push([zoneId, recordId]);
        return Promise.resolve({ ok: true });
      }),
    );
    using _fetch = stubFetch(new Response(JSON.stringify({ Id: 12 })));

    expect(await bunnyCdnApi.registerBunnySubdomain("child")).toEqual({
      error: "certificate pending",
      ok: false,
    });
    expect(validations).toEqual(Array(5).fill("child.tickets.example.test"));
    expect(delays).toEqual([5_000, 10_000, 15_000, 20_000]);
    expect(deleted).toEqual([["7", 12]]);
  });

  test("does not clean up an unknown DNS record id", async () => {
    const deleted: number[] = [];
    stubAvailableSubdomain(stubs);
    stubs.push(
      stub(bunnyCdnApi, "validateCustomDomain", () =>
        Promise.resolve({ error: "certificate pending", ok: false }),
      ),
      stub(bunnyCdnApi, "delay", () => Promise.resolve()),
      stub(bunnyCdnApi, "deleteDnsRecord", (_zoneId, recordId) => {
        deleted.push(recordId);
        return Promise.resolve({ ok: true });
      }),
    );
    using _fetch = stubFetch(new Response("not json"));

    await bunnyCdnApi.registerBunnySubdomain("child");

    expect(deleted).toEqual([]);
  });

  test("deletes a DNS record with the exact provider request", async () => {
    using fetchStub = stubFetch(new Response(null, { status: 204 }));

    expect(await bunnyCdnApi.deleteDnsRecord("7", 12)).toEqual({ ok: true });
    const [url, init] = fetchStub.calls[0]!.args as [string, RequestInit];
    expect(url).toBe("https://api.bunny.net/dnszone/7/records/12");
    expect(init.method).toBe("DELETE");
  });

  test("labels a DNS record delete failure", async () => {
    using _fetch = stubFetch(new Response("failed", { status: 500 }));
    expect(await bunnyCdnApi.deleteDnsRecord("7", 12)).toEqual({
      error: "Delete DNS record failed (500): failed",
      ok: false,
    });
  });
});