Ctrl K
ComunidadGuías…

test/shared/deno-deploy-api.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 { it as test } from "@std/testing/bdd";
import { stub } from "@std/testing/mock";
import { denoDeployApi, denoHostingProvider } from "#shared/deno-deploy-api.ts";
import { okResult } from "#shared/result.ts";
import { expectIncludesAll } from "#test-utils/assertions.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { stubFetch } from "#test-utils/fetch-stub.ts";
import { withVirtualBackoff } from "#test-utils/virtual-time.ts";

const DENO_ENV = {
  DENO_DEPLOY_ORG_ID: "test-org-id",
  DENO_DEPLOY_ORG_SLUG: "test-org",
  DENO_DEPLOY_TOKEN: "test-deno-token",
};

interface CapturedRequest {
  body: unknown;
  contentType?: string | null;
  method?: string;
  url: string | undefined;
}

/** Capture the request URL + JSON body and respond with `responseBody`. */
const captureRequest =
  (responseBody: unknown, captured: CapturedRequest) =>
  (url: string, init?: RequestInit): Response => {
    captured.url = url;
    captured.body = JSON.parse(init?.body as string);
    captured.contentType = new Headers(init?.headers).get("content-type");
    if (init?.method !== undefined) captured.method = init.method;
    return new Response(JSON.stringify(responseBody));
  };

describeWithEnv("deno-deploy-api", { env: DENO_ENV }, () => {
  // ── createApp ──────────────────────────────────────────────────────────────

  test("createApp POSTs to /v2/apps with orgId and slug", async () => {
    const captured: CapturedRequest = { body: undefined, url: undefined };
    using _fetch = stubFetch(
      captureRequest({ id: "app_abc123", slug: "my-app" }, captured),
    );
    const result = await denoDeployApi.createApp("my-app");
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.appId).toBe("app_abc123");
      expect(result.value.slug).toBe("my-app");
    }
    assertStringIncludes(captured.url ?? "", "/v2/apps");
    expect(captured.body).toEqual({ orgId: "test-org-id", slug: "my-app" });
    expect(captured.contentType).toBe("application/json");
    expect(captured.method).toBe("POST");
  });

  test("createApp uses Bearer auth header", async () => {
    let authHeader: string | undefined;

    using _fetch = stubFetch((_url, init) => {
      authHeader = (init?.headers as Record<string, string>)?.Authorization;
      return new Response(JSON.stringify({ id: "app_auth", slug: "auth-app" }));
    });
    await denoDeployApi.createApp("auth-app");
    expect(authHeader).toBe("Bearer test-deno-token");
  });

  test("createApp returns error when API responds with failure", async () => {
    using _fetch = stubFetch(
      new Response(JSON.stringify({ message: "Invalid slug" }), {
        status: 400,
      }),
    );
    const result = await denoDeployApi.createApp("bad");
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expectIncludesAll(result.error, [
        "Create app failed (400)",
        "Invalid slug",
      ]);
    }
  });

  // ── setEnvVars ─────────────────────────────────────────────────────────────

  test("setEnvVars PATCHes only the supplied vars (no GET)", async () => {
    const captured: CapturedRequest = { body: undefined, url: undefined };
    let callCount = 0;

    using _fetch = stubFetch((url, init) => {
      callCount++;
      return captureRequest({ id: "app_ev", slug: "env-app" }, captured)(
        url,
        init,
      );
    });
    const result = await denoDeployApi.setEnvVars("app_ev", [
      ["NEW_VAR", "new-value"],
    ]);
    expect(result.ok).toBe(true);
    expect(callCount).toBe(1);
    assertStringIncludes(captured.url ?? "", "/apps/app_ev");
    expect(captured.method).toBe("PATCH");
    expect(captured.body).toEqual({
      env_vars: [
        {
          contexts: ["production"],
          key: "NEW_VAR",
          secret: true,
          value: "new-value",
        },
      ],
    });
  });

  test("setEnvVars returns error when PATCH fails", async () => {
    using _fetch = stubFetch(
      new Response(JSON.stringify({ error: "invalid env var name" }), {
        status: 422,
      }),
    );
    const result = await denoDeployApi.setEnvVars("app_pe", [["BAD VAR", "v"]]);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expectIncludesAll(result.error, [
        "Set app env vars failed (422)",
        "invalid env var name",
      ]);
    }
  });
  test("setEnvVar PATCHes one secret, production entry by default", async () => {
    const captured: CapturedRequest = { body: undefined, url: undefined };
    using _fetch = stubFetch(captureRequest({ env_vars: [] }, captured));
    const result = await denoDeployApi.setEnvVar("app_sv", {
      key: "SUPPORT_PAGE_TEXT",
      value: "# Hi",
    });
    expect(result.ok).toBe(true);
    expect(captured.method).toBe("PATCH");
    expect(captured.url).toBe("https://api.deno.com/v2/apps/app_sv");
    expect(captured.body).toEqual({
      env_vars: [
        {
          contexts: ["production"],
          key: "SUPPORT_PAGE_TEXT",
          secret: true,
          value: "# Hi",
        },
      ],
    });
  });
  test("setEnvVar passes a plain entry's own secret flag and contexts", async () => {
    const captured: CapturedRequest = { body: undefined, url: undefined };
    using _fetch = stubFetch(captureRequest({ env_vars: [] }, captured));
    const result = await denoDeployApi.setEnvVar("app_plain", {
      contexts: ["preview"],
      key: "SUPPORT_PAGE_TEXT",
      secret: false,
      value: "# Hi",
    });
    expect(result.ok).toBe(true);
    expect(captured.url).toBe("https://api.deno.com/v2/apps/app_plain");
    expect(captured.method).toBe("PATCH");
    expect(captured.body).toEqual({
      env_vars: [
        {
          contexts: ["preview"],
          key: "SUPPORT_PAGE_TEXT",
          secret: false,
          value: "# Hi",
        },
      ],
    });
  });

  test("setEnvVar returns the labelled error when PATCH fails", async () => {
    using _fetch = stubFetch(new Response("{}", { status: 401 }));
    const result = await denoDeployApi.setEnvVar("app_svf", {
      key: "SUPPORT_PAGE_TEXT",
      value: "# Hi",
    });
    expect(result.ok).toBe(false);
    if (!result.ok) {
      assertStringIncludes(result.error, "Set app env var failed (401)");
    }
  });

  test("getAppEnvVars returns the labelled error when GET fails", async () => {
    using _fetch = stubFetch(new Response("{}", { status: 401 }));
    const result = await denoDeployApi.getAppEnvVars("app_gev");
    expect(result.ok).toBe(false);
    if (!result.ok) {
      assertStringIncludes(result.error, "Get app failed (401)");
    }
  });

  // ── deployCode ─────────────────────────────────────────────────────────────

  test("deployCode POSTs assets and config to the revision endpoint", async () => {
    const captured: CapturedRequest = { body: undefined, url: undefined };
    using _fetch = stubFetch(
      captureRequest({ id: "dep_123", status: "succeeded" }, captured),
    );
    const result = await denoDeployApi.deployCode(
      "app_dc",
      "console.log('hello')",
    );
    expect(result).toEqual({ ok: true, value: undefined });
    expect(captured.url).toBe("https://api.deno.com/v2/apps/app_dc/deploy");
    expect(captured.method).toBe("POST");
    expect(captured.body).toEqual({
      assets: {
        "main.ts": {
          content: "console.log('hello')",
          encoding: "utf-8",
          kind: "file",
        },
      },
      config: { runtime: { entrypoint: "main.ts", type: "dynamic" } },
      production: true,
    });
  });

  test("deployCode waits for an accepted revision to succeed", async () => {
    const requests: string[] = [];
    const requestTimes: number[] = [];
    const responses = [
      { id: "revision-1", status: "queued" },
      { id: "revision-1", status: "building" },
      { id: "revision-1", status: "succeeded" },
    ];
    using _fetch = stubFetch((url) => {
      requests.push(String(url));
      requestTimes.push(Date.now());
      return Response.json(responses.shift());
    });

    expect(
      await withVirtualBackoff(() => denoDeployApi.deployCode("app_1", "code")),
    ).toEqual({ ok: true, value: undefined });
    expect(requests).toEqual([
      "https://api.deno.com/v2/apps/app_1/deploy",
      "https://api.deno.com/v2/revisions/revision-1",
      "https://api.deno.com/v2/revisions/revision-1",
    ]);
    expect(requestTimes.map((time) => time - requestTimes[0]!)).toEqual([
      0, 1_000, 2_000,
    ]);
  });

  test("deployCode reports a failed accepted revision", async () => {
    using _fetch = stubFetch(
      Response.json({
        failure_reason: "error",
        id: "revision-failed",
        status: "failed",
      }),
    );

    expect(await denoDeployApi.deployCode("app_1", "code")).toEqual({
      error: "Deno revision revision-failed failed: error",
      ok: false,
    });
  });

  test("deployCode reports a skipped accepted revision", async () => {
    using _fetch = stubFetch(
      Response.json({ id: "revision-skipped", status: "skipped" }),
    );

    expect(await denoDeployApi.deployCode("app_1", "code")).toEqual({
      error: "Deno revision revision-skipped skipped: skipped",
      ok: false,
    });
  });

  test("deployCode reports a revision that never finishes", async () => {
    using _fetch = stubFetch(() =>
      Response.json({ id: "revision-pending", status: "queued" }),
    );

    expect(
      await withVirtualBackoff(() => denoDeployApi.deployCode("app_1", "code")),
    ).toEqual({
      error: "Deno revision revision-pending did not finish within 20 seconds",
      ok: false,
    });
  });

  test("deployCode reports repeated revision read failures", async () => {
    const calls = { value: 0 };
    using _fetch = stubFetch(() => {
      calls.value += 1;
      return calls.value === 1
        ? Response.json({ id: "revision-unreadable", status: "queued" })
        : new Response("unavailable", { status: 503 });
    });

    expect(
      await withVirtualBackoff(() => denoDeployApi.deployCode("app_1", "code")),
    ).toEqual({
      error:
        "Deno revision revision-unreadable could not be read: Get revision failed (503): unavailable",
      ok: false,
    });
  });

  test("deployCode rejects an undocumented revision response", async () => {
    using _fetch = stubFetch(
      Response.json({ id: "revision-unknown", status: "running" }),
    );

    await expect(denoDeployApi.deployCode("app_1", "code")).rejects.toThrow();
  });

  test("deployCode returns error when API responds with failure", async () => {
    using _fetch = stubFetch(
      new Response(JSON.stringify({ message: "app not found" }), {
        status: 404,
      }),
    );
    const result = await denoDeployApi.deployCode("app_missing", "code");
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expectIncludesAll(result.error, [
        "Deploy code failed (404)",
        "app not found",
      ]);
    }
  });

  test("hosting provider names its required credential", () => {
    expect(denoHostingProvider.configEnvVar).toBe("DENO_DEPLOY_TOKEN");
  });

  test("hosting provider builds the Deno v2 production domain", async () => {
    using _create = stub(denoDeployApi, "createApp", () =>
      Promise.resolve(okResult({ appId: "app_1", slug: "child" })),
    );
    using _secrets = stub(denoDeployApi, "setEnvVars", () =>
      Promise.resolve(okResult(undefined)),
    );

    expect(await denoHostingProvider.prepareSite("Child", "code", [])).toEqual({
      ok: true,
      value: {
        defaultHostname: "https://child.test-org.deno.net",
        hostingId: "app_1",
      },
    });
  });

  test("hosting publish returns only provider success", async () => {
    using _fetch = stubFetch(Response.json({ id: "dep", status: "succeeded" }));

    expect(await denoHostingProvider.publishSite("app_1", "code")).toEqual({
      ok: true,
    });
  });

  test("hosting publish preserves provider failure", async () => {
    using _fetch = stubFetch(new Response("failed", { status: 500 }));

    expect(await denoHostingProvider.publishSite("app_1", "code")).toEqual({
      error: "Deploy code failed (500): failed",
      ok: false,
    });
  });
});