Ctrl K
ComunidadGuías…

test/scripts/static-cdn-publish.test.ts

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

Archivo original ↗Preguntar a KiBot
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import { FakeTime } from "@std/testing/time";
import {
  publishStaticCdnAssets,
  type StaticCdnAsset,
} from "#scripts/static-cdn.ts";
import {
  assetResponse,
  CONFIG,
  STYLE_ASSET,
  successfulFetcher,
  verificationFetcher,
  WASM_ASSET,
} from "./static-cdn-fixtures.ts";

const STALLED_REQUESTS = [
  { method: "PUT", stage: "upload" },
  { method: "POST", stage: "purge" },
  { method: "GET", stage: "verification" },
] as const;

const styleOrWasm =
  (style: StaticCdnAsset) =>
  (url: string): StaticCdnAsset =>
    url.endsWith(STYLE_ASSET.filename) ? style : WASM_ASSET;

const completeRequest = successfulFetcher(() => STYLE_ASSET);

const rejectWhenAborted = (signal: AbortSignal): Promise<Response> =>
  new Promise((_resolve, reject) => {
    signal.addEventListener("abort", () => reject(signal.reason), {
      once: true,
    });
  });

const stalledFetcher =
  (stalledMethod: string, onStart: () => void): typeof fetch =>
  (input, init) => {
    const method = init?.method ?? "GET";
    if (method !== stalledMethod) return completeRequest(input, init);
    const signal = init?.signal;
    if (!signal) throw new Error("Expected bounded Bunny request");
    const stalled = rejectWhenAborted(signal);
    onStart();
    return stalled;
  };

describe("publishStaticCdnAssets", () => {
  test("uploads one immutable release and returns its public URLs", async () => {
    const requests: Request[] = [];
    const assetForUrl = styleOrWasm(STYLE_ASSET);
    const fetcher: typeof fetch = (input, init) => {
      const request = new Request(input, init);
      requests.push(request);
      const asset = assetForUrl(request.url);
      return Promise.resolve(
        request.method === "GET"
          ? assetResponse(asset)
          : new Response(null, { status: 201 }),
      );
    };

    const published = await publishStaticCdnAssets(
      CONFIG,
      [STYLE_ASSET, WASM_ASSET],
      fetcher,
    );

    expect(published.origin).toBe("https://assets.example.com");
    expect(published.urls["style.css"]).toBe(
      "https://assets.example.com/static/assets/d4632641703f36d87d3ae3a6fd8f4849ad16f9ff42795ad26f25c3fe42a8bf1e/style.css",
    );
    expect(published.urls["jpegDec.wasm"]).toBe(
      "https://assets.example.com/static/assets/d4632641703f36d87d3ae3a6fd8f4849ad16f9ff42795ad26f25c3fe42a8bf1e/jpegDec.wasm",
    );
    expect(requests.length).toBe(5);
    const cssUploadUrl =
      "https://storage.bunnycdn.com/tickets-assets/static/assets/" +
      "d4632641703f36d87d3ae3a6fd8f4849ad16f9ff42795ad26f25c3fe42a8bf1e/style.css";
    const cssUpload = requests.find(({ url }) => url === cssUploadUrl);
    if (!cssUpload) throw new Error("Expected style.css upload request");
    expect(cssUpload.method).toBe("PUT");
    expect(cssUpload.headers.get("accesskey")).toBe("storage-secret");
    expect(cssUpload.headers.get("content-type")).toBe(
      "text/css; charset=utf-8",
    );
    expect(cssUpload.headers.get("checksum")).toBe(
      "7C98040A541657584690AE2A1CC3B42A8B53B159CC60C5D3ABBFECBAEAC6C94A",
    );
    expect(cssUpload.url).toBe(cssUploadUrl);
    expect(requests[2]?.url).toBe(
      "https://api.bunny.net/pullzone/12345/purgeCache",
    );
    expect(requests[2]?.method).toBe("POST");
    expect(requests[2]?.headers.get("accesskey")).toBe("account-secret");
    expect(requests[3]?.method).toBe("GET");
    expect(requests[3]?.url).toBe(published.urls[STYLE_ASSET.filename]);
    expect(requests[4]?.url).toBe(published.urls[WASM_ASSET.filename]);
  });

  test("release URLs ignore input order and change with content", async () => {
    const original = await publishStaticCdnAssets(
      CONFIG,
      [STYLE_ASSET, WASM_ASSET],
      successfulFetcher(styleOrWasm(STYLE_ASSET)),
    );
    const reversed = await publishStaticCdnAssets(
      CONFIG,
      [WASM_ASSET, STYLE_ASSET],
      successfulFetcher(styleOrWasm(STYLE_ASSET)),
    );
    const changedStyle = {
      ...STYLE_ASSET,
      bytes: new TextEncoder().encode("x"),
    };
    const changed = await publishStaticCdnAssets(
      CONFIG,
      [changedStyle, WASM_ASSET],
      successfulFetcher(styleOrWasm(changedStyle)),
    );

    expect(reversed.urls).toEqual(original.urls);
    expect(changed.urls[STYLE_ASSET.filename]).not.toBe(
      original.urls[STYLE_ASSET.filename],
    );
  });

  test("release URLs change with content type", async () => {
    const changedType = {
      ...STYLE_ASSET,
      contentType: "text/x-css; charset=utf-8",
    };
    const original = await publishStaticCdnAssets(
      CONFIG,
      [STYLE_ASSET],
      successfulFetcher(() => STYLE_ASSET),
    );
    const changed = await publishStaticCdnAssets(
      CONFIG,
      [changedType],
      successfulFetcher(() => changedType),
    );

    expect(changed.urls[STYLE_ASSET.filename]).not.toBe(
      original.urls[STYLE_ASSET.filename],
    );
  });

  test("uploads through the configured regional Bunny host", async () => {
    let uploadUrl = "";
    await publishStaticCdnAssets(
      { ...CONFIG, storageHost: "uk.storage.bunnycdn.com" },
      [STYLE_ASSET],
      (input, init) => {
        const method = init?.method ?? "GET";
        if (method === "PUT") uploadUrl = String(input);
        return Promise.resolve(
          method === "GET"
            ? assetResponse(STYLE_ASSET)
            : new Response(null, { status: 201 }),
        );
      },
    );

    expect(uploadUrl).toMatch(
      /^https:\/\/uk\.storage\.bunnycdn\.com\/tickets-assets\/static\/assets\/[a-f0-9]{64}\/style\.css$/,
    );
  });

  test("fails the build when Bunny Storage rejects an upload", async () => {
    await expect(
      publishStaticCdnAssets(
        CONFIG,
        [
          {
            bytes: new Uint8Array([1]),
            contentType: "application/javascript",
            filename: "admin.js",
          },
        ],
        () => Promise.resolve(new Response("denied", { status: 401 })),
      ),
    ).rejects.toThrow("admin.js");
  });

  test("rejects duplicate filenames before uploading", async () => {
    const asset = {
      bytes: new Uint8Array([1]),
      contentType: "application/javascript",
      filename: "admin.js",
    };
    let requested = false;
    await expect(
      publishStaticCdnAssets(CONFIG, [asset, asset], () => {
        requested = true;
        return Promise.reject(new Error("Unexpected network request"));
      }),
    ).rejects.toThrow("must be unique");
    expect(requested).toBe(false);
  });

  test("fails the build when the pull-zone purge fails", async () => {
    await expect(
      publishStaticCdnAssets(
        { ...CONFIG, cdnUrl: "https://assets.example.com" },
        [],
        (input) =>
          Promise.resolve(
            new Response(null, {
              status: String(input).includes("purgeCache") ? 503 : 201,
            }),
          ),
      ),
    ).rejects.toThrow("Failed to purge static CDN: HTTP 503");
  });

  test("fails the build when a public CDN asset is unavailable", async () => {
    await expect(
      publishStaticCdnAssets(
        CONFIG,
        [STYLE_ASSET],
        verificationFetcher(new Response(null, { status: 404 })),
      ),
    ).rejects.toThrow("style.css: HTTP 404");
  });

  test("fails the build when public CDN bytes differ", async () => {
    await expect(
      publishStaticCdnAssets(
        CONFIG,
        [STYLE_ASSET],
        verificationFetcher(
          new Response("different", {
            headers: { "content-type": STYLE_ASSET.contentType },
          }),
        ),
      ),
    ).rejects.toThrow("style.css does not match the uploaded file");
  });

  test("fails the build when the public content type differs", async () => {
    await expect(
      publishStaticCdnAssets(
        CONFIG,
        [STYLE_ASSET],
        verificationFetcher(
          new Response(STYLE_ASSET.bytes, {
            headers: { "content-type": "application/octet-stream" },
          }),
        ),
      ),
    ).rejects.toThrow(
      "Static CDN asset style.css has content type application/octet-stream; expected text/css",
    );
  });

  test("fails the build when the public content type is missing", async () => {
    await expect(
      publishStaticCdnAssets(
        CONFIG,
        [STYLE_ASSET],
        verificationFetcher(new Response(STYLE_ASSET.bytes)),
      ),
    ).rejects.toThrow("Static CDN asset style.css is missing its content type");
  });

  test("allows Bunny requests to complete before the deadline", async () => {
    const fetcher: typeof fetch = (_input, init) => {
      const signal = init?.signal;
      if (!signal) throw new Error("Expected bounded Bunny request");
      const response =
        init?.method === undefined
          ? assetResponse(STYLE_ASSET)
          : new Response(null, { status: 201 });
      return new Promise((resolve, reject) => {
        const onAbort = (): void => {
          clearTimeout(timer);
          reject(signal.reason);
        };
        const timer = setTimeout(() => {
          signal.removeEventListener("abort", onAbort);
          resolve(response);
        }, 5);
        signal.addEventListener("abort", onAbort, { once: true });
      });
    };

    const published = await publishStaticCdnAssets(
      CONFIG,
      [STYLE_ASSET],
      fetcher,
    );
    assertStringIncludes(
      published.urls[STYLE_ASSET.filename] ?? "",
      "/style.css",
    );
  });

  for (const { method: stalledMethod, stage } of STALLED_REQUESTS) {
    test(`aborts a stalled ${stage} request after 30 seconds`, async () => {
      using time = new FakeTime();
      const started = Promise.withResolvers<void>();
      let error: unknown;
      let settled = false;
      const publishing = publishStaticCdnAssets(
        CONFIG,
        [STYLE_ASSET],
        stalledFetcher(stalledMethod, () => started.resolve()),
      );
      const observed = publishing
        .catch((caught: unknown) => {
          error = caught;
        })
        .finally(() => {
          settled = true;
        });

      await started.promise;
      await time.tickAsync(29_999);
      expect(settled).toBe(false);
      await time.tickAsync(1);
      await observed;
      if (!(error instanceof DOMException)) {
        throw new Error(`Expected ${stage} timeout DOMException`);
      }
      expect(error.name).toBe("TimeoutError");
    });
  }
});