Ctrl K
ComunidadGuías…

test/shared/builder.test.ts

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

Archivo original ↗Preguntar a KiBot
import { assert, assertArrayIncludes, assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { it as test } from "@std/testing/bdd";
import { stub } from "@std/testing/mock";
import { builderApi } from "#shared/builder.ts";
import { bunnyDbProvider as bunnyDbApi } from "#shared/bunny-db.ts";
import { okResult } from "#shared/result.ts";
import { tursoDbProvider as tursoApi } from "#shared/turso-api.ts";
import {
  expectBuildError,
  expectSecret,
  MOCK_DB_RESULT,
  secretsFrom,
  stubBuilderFetch,
  stubBuildSiteApis,
  stubDenoBuilderApis,
  withBuildSiteMocks,
} from "#test-utils/builder-mocks.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { withEnv } from "#test-utils/env.ts";
import { stubFetch } from "#test-utils/fetch-stub.ts";
import { withMocks } from "#test-utils/mocks.ts";

const BUILD_INPUT = {
  dbToken: "token123",
  dbUrl: "libsql://test.turso.io",
  siteName: "Test",
} as const;

const buildSite = (input: Parameters<typeof builderApi.buildSite>[0]) =>
  builderApi.buildSite(input, () => Promise.resolve(0));

type Restorable = { restore(): void };

/** Each error path: install mocks, call buildSite, expect a matching failure. */
const ERROR_CASES: {
  name: string;
  mocks: () => Restorable | Record<string, Restorable>;
  input: Parameters<typeof builderApi.buildSite>[0];
  error: string;
}[] = [
  {
    error: "No release asset",
    input: BUILD_INPUT,
    mocks: () => stubBuilderFetch(undefined, { assets: [] }),
    name: "buildSite returns error when release has no asset",
  },
  {
    error: "Failed to fetch release",
    input: BUILD_INPUT,
    mocks: () => stubFetch(new Response("Not Found", { status: 404 })),
    name: "buildSite returns error when GitHub API fails",
  },
  {
    error: "Failed to download release",
    input: BUILD_INPUT,
    mocks: () =>
      stubBuilderFetch(() => new Response("Not Found", { status: 404 })),
    name: "buildSite returns error when release download fails",
  },
  {
    error: "Create edge script failed",
    input: BUILD_INPUT,
    mocks: () =>
      stubBuildSiteApis({
        createResult: {
          error: "Create edge script failed (500): Server Error",
          ok: false,
        },
      }),
    name: "buildSite returns error when edge script creation fails",
  },
  {
    error: "Update pull zone failed",
    input: BUILD_INPUT,
    mocks: () =>
      stubBuildSiteApis({
        updatePullZoneResult: {
          error: "Update pull zone failed (500): Server Error",
          ok: false,
        },
      }),
    name: "buildSite returns error when pull zone update fails",
  },
  {
    error: "Failed to set secrets",
    input: BUILD_INPUT,
    mocks: () =>
      stubBuildSiteApis({
        secretResult: {
          error: "Set secret DB_URL failed (403): Forbidden",
          ok: false,
        },
      }),
    name: "buildSite returns error when secret setting fails",
  },
  {
    error: "Publish edge script failed",
    input: BUILD_INPUT,
    mocks: () =>
      stubBuildSiteApis({
        publishResult: {
          error: "Publish edge script failed (500): Error",
          ok: false,
        },
      }),
    name: "buildSite returns error when publish fails",
  },
  {
    error: "Create database failed",
    input: { siteName: "Fail Site" },
    mocks: () =>
      stubBuildSiteApis({
        createDbResult: {
          error: "Create database failed (403): Forbidden",
          ok: false,
        },
      }),
    name: "buildSite returns error when auto-create database fails",
  },
];

describeWithEnv(
  "builder",
  {
    db: true,
    env: {
      ADMIN_EMAIL_ADDRESS: "admin@example.com",
      DENO_DEPLOY_ORG_SLUG: "test-org",
      NTFY_URL: "https://ntfy.example.com/test",
      SENTRY_URL: "https://k@bugs.example.com/2",
      SUPPORT_FORM_NAG_DAYS: "14",
      SUPPORT_PAGE_TEXT: "# Help\\n\\nAsk us anything",
    },
  },
  () => {
    test("generateEncryptionKey returns 32-byte base64 string", () => {
      const key = builderApi.generateEncryptionKey();
      const bytes = Uint8Array.from(atob(key), (c) => c.charCodeAt(0));
      expect(bytes.length).toBe(32);
    });

    test("testDbConnection succeeds with valid in-memory database", async () => {
      const result = await builderApi.testDbConnection(":memory:", "");
      expect(result.ok).toBe(true);
    });

    test("testDbConnection returns error for invalid URL", async () => {
      const result = await builderApi.testDbConnection(
        "libsql://nonexistent.invalid.host.example",
        "bad-token",
      );
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error).toBeTruthy();
      }
    });

    for (const { name, mocks, input, error } of ERROR_CASES) {
      test(name, async () => {
        await withMocks(mocks, async () => {
          const result = await buildSite(input);
          expectBuildError(result, error);
        });
      });
    }

    test("buildSite copies host secrets when env vars are set", () =>
      withBuildSiteMocks(async ({ secretStub }) => {
        const result = await buildSite(BUILD_INPUT);
        expect(result.ok).toBe(true);

        const secretsSet = secretsFrom(secretStub);
        expectSecret(secretsSet, "NTFY_URL", "https://ntfy.example.com/test");
        expectSecret(secretsSet, "SENTRY_URL", "https://k@bugs.example.com/2");
        expectSecret(secretsSet, "ADMIN_EMAIL_ADDRESS", "admin@example.com");
        // Bunny sites keep the support message as a readable variable
        // instead — the seed test below pins that call.
        expect(
          secretsSet.map(([name]) => name).includes("SUPPORT_PAGE_TEXT"),
        ).toBe(false);
        expectSecret(secretsSet, "SUPPORT_FORM_NAG_DAYS", "14");
      }));

    test("buildSite succeeds with all steps", () =>
      withBuildSiteMocks(
        async ({ createStub, updatePzStub, publishStub, secretStub }) => {
          const result = await buildSite({
            ...BUILD_INPUT,
            siteName: "My Site",
          });

          expect(result.ok).toBe(true);
          if (result.ok) {
            expect(result.hostingId).toBe("42");
            expect(result.defaultHostname).toBe("https://test-42.b-cdn.net");
          }

          expect(createStub.calls[0]!.args[0]).toBe("Tickets - My Site");

          expect(updatePzStub.calls.length).toBe(1);
          expect(updatePzStub.calls[0]!.args[0]).toBe(99);
          expect(updatePzStub.calls[0]!.args[1]).toEqual({
            DisableCookies: false,
          });

          const secretsSet = secretsFrom(secretStub);
          const secretNames = secretsSet.map(([name]) => name);
          assertArrayIncludes(secretNames, ["DB_URL"]);
          assertArrayIncludes(secretNames, ["DB_TOKEN"]);
          assertArrayIncludes(secretNames, ["DB_ENCRYPTION_KEY"]);
          assertArrayIncludes(secretNames, ["BUNNY_SCRIPT_ID"]);

          expectSecret(secretsSet, "DB_URL", "libsql://test.turso.io");
          expectSecret(secretsSet, "BUNNY_SCRIPT_ID", "42");

          expect(publishStub.calls.length).toBe(1);
          expect(publishStub.calls[0]!.args[0]).toBe(42);
        },
      ));

    test("buildSite auto-creates database when dbUrl is not provided", () =>
      withBuildSiteMocks(async ({ createDbStub, secretStub }) => {
        const result = await buildSite({ siteName: "Auto Site" });

        expect(result.ok).toBe(true);
        expect(createDbStub.calls.length).toBe(1);
        expect(createDbStub.calls[0]!.args[0]).toBe("Auto Site");

        if (result.ok) {
          expect(result.dbUrl).toBe(MOCK_DB_RESULT.value.dbUrl);
          expect(result.dbToken).toBe(MOCK_DB_RESULT.value.dbToken);
        }

        const secretsSet = secretsFrom(secretStub);
        expectSecret(secretsSet, "DB_URL", MOCK_DB_RESULT.value.dbUrl);
        expectSecret(secretsSet, "DB_TOKEN", MOCK_DB_RESULT.value.dbToken);
      }));

    test("buildSite uses provided dbUrl and dbToken without calling createDatabase", () =>
      withBuildSiteMocks(async ({ createDbStub }) => {
        await buildSite({
          dbToken: "provided-token",
          dbUrl: "libsql://provided.io",
          siteName: "Provided",
        });
        expect(createDbStub.calls.length).toBe(0);
      }));

    test("buildSite uses provided code without fetching from GitHub", () =>
      withBuildSiteMocks(async ({ createStub, fetchStub }) => {
        const result = await buildSite({
          code: "console.log('local-bundle')",
          siteName: "Local",
        });
        expect(result.ok).toBe(true);

        const fetchedUrls = fetchStub.calls.map((c) => String(c.args[0]));
        expect(fetchedUrls.some((u) => u.includes("github"))).toBe(false);

        const deployedCode = createStub.calls.map((c) => c.args[1]);
        expect(deployedCode).toEqual(["console.log('local-bundle')"]);
      }));

    test("buildSite uses empty string dbToken when dbUrl provided without dbToken", () =>
      withBuildSiteMocks(async ({ createDbStub, secretStub }) => {
        await buildSite({
          dbUrl: "libsql://provided.io",
          siteName: "NoToken",
        });
        expect(createDbStub.calls.length).toBe(0);
        expectSecret(secretsFrom(secretStub), "DB_TOKEN", "");
      }));

    test("buildSite succeeds on Deno Deploy hosting", () =>
      withMocks(
        () => stubDenoBuilderApis(),
        async ({ deployStub }) => {
          const result = await buildSite({
            dbToken: "tok",
            dbUrl: "libsql://test.turso.io",
            hostingProvider: "deno",
            siteName: "Test",
          });
          expect(result.ok).toBe(true);
          if (result.ok) {
            expect(result.hostingProvider).toBe("deno");
            expect(result.hostingId).toBe("app_abc123");
            expect(result.defaultHostname).toBe(
              "https://tickets-test.test-org.deno.net",
            );
          }
          expect(deployStub.calls).toHaveLength(1);
        },
      ));

    const DENO_ERROR_CASES: {
      name: string;
      mocks: () => Restorable | Record<string, Restorable>;
      error: string;
    }[] = [
      {
        error: "Create app failed",
        mocks: () =>
          stubDenoBuilderApis({
            createAppResult: {
              error: "Create app failed (500): Error",
              ok: false as const,
            },
          }),
        name: "buildSite returns error when Deno app creation fails",
      },
      {
        error: "Failed to set secrets",
        mocks: () =>
          stubDenoBuilderApis({
            setEnvResult: {
              error: "Set env failed (403)",
              ok: false as const,
            },
          }),
        name: "buildSite returns error when Deno setEnvVars fails",
      },
      {
        error: "Deploy failed",
        mocks: () =>
          stubDenoBuilderApis({
            deployResult: {
              error: "Deploy failed (500)",
              ok: false as const,
            },
          }),
        name: "buildSite returns error when Deno deployCode fails",
      },
    ];
    for (const { name, mocks, error } of DENO_ERROR_CASES) {
      test(name, () =>
        withMocks(mocks, async () => {
          const result = await buildSite({
            dbToken: "tok",
            dbUrl: "libsql://test.io",
            hostingProvider: "deno",
            siteName: "Fail",
          });
          expectBuildError(result, error);
        }),
      );
    }

    test("createDatabase dispatches to tursoApi when provider is turso", () =>
      withMocks(
        () =>
          stub(tursoApi, "createDatabase", () =>
            Promise.resolve(
              okResult({
                dbId: "turso_db_123",
                dbToken: "turso-token",
                dbUrl: "libsql://turso.io",
              }),
            ),
          ),
        async (tursoStub) => {
          const result = await builderApi.createDatabase("My Site", "turso");
          expect(result.ok).toBe(true);
          if (result.ok) assertStringIncludes(result.value.dbUrl, "turso.io");
          expect(tursoStub.calls).toHaveLength(1);
        },
      ));

    test("createDatabase dispatches to bunnyDbApi when provider is bunny", () =>
      withMocks(
        () =>
          stub(bunnyDbApi, "createDatabase", () =>
            Promise.resolve(
              okResult({
                dbId: "bunny_db_456",
                dbToken: "bunny-token",
                dbUrl: "libsql://bunny.io",
              }),
            ),
          ),
        async (bunnyStub) => {
          const result = await builderApi.createDatabase("My Site", "bunny");
          expect(result.ok).toBe(true);
          if (result.ok) assertStringIncludes(result.value.dbUrl, "bunny.io");
          expect(bunnyStub.calls).toHaveLength(1);
        },
      ));

    test("buildSite auto-creates turso database when dbProvider is turso", () =>
      withMocks(
        () => ({
          ...stubBuildSiteApis(),
          tursoStub: stub(tursoApi, "createDatabase", () =>
            Promise.resolve(
              okResult({
                dbId: "turso_auto",
                dbToken: "turso-tok",
                dbUrl: "libsql://auto.turso.io",
              }),
            ),
          ),
        }),
        async ({ tursoStub }) => {
          const result = await buildSite({
            dbProvider: "turso",
            siteName: "Turso Auto",
          });
          expect(result.ok).toBe(true);
          expect(tursoStub.calls).toHaveLength(1);
          if (result.ok) {
            expect(result.dbProvider).toBe("turso");
            expect(result.dbUrl).toBe("libsql://auto.turso.io");
          }
        },
      ));

    test("buildSite on Deno does not include Bunny DNS secrets in env vars", async () => {
      using _env = withEnv({
        BUNNY_API_KEY: "host-bunny-key",
        BUNNY_DNS_SUBDOMAIN_SUFFIX: ".tickets",
        BUNNY_DNS_ZONE_ID: "zone-123",
      });
      await withMocks(
        () => stubDenoBuilderApis(),
        async ({ setEnvStub }) => {
          const result = await buildSite({
            dbToken: "tok",
            dbUrl: "libsql://test.turso.io",
            hostingProvider: "deno",
            siteName: "Deno Site",
          });
          expect(result.ok).toBe(true);
          const secrets = setEnvStub.calls[0]!.args[1] as [string, string][];
          const names = secrets.map(([name]) => name);
          assert(!names.includes("BUNNY_API_KEY"));
          assert(!names.includes("BUNNY_DNS_ZONE_ID"));
          assert(!names.includes("BUNNY_DNS_SUBDOMAIN_SUFFIX"));
        },
      );
    });
  },
);