Ctrl K
ComunidadGuías…

src/lib/runtime-config.test.ts

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

Archivo original ↗Preguntar a KiBot
import { afterEach, describe, expect, it, vi } from "vitest";

import {
  DEFAULT_SITE_URL,
  publicConfig,
  readRuntimeConfig,
} from "./runtime-config";
import {
  DEFAULT_BASEMAP_ATTRIBUTION,
  DEFAULT_STYLE_URL,
  DEFAULT_STYLE_URL_DARK,
  MISSING_ATTRIBUTION_MESSAGE,
} from "./basemap";

describe("readRuntimeConfig", () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("configures nothing from an empty environment", () => {
    const config = readRuntimeConfig({});

    expect(config).toEqual({
      siteUrl: DEFAULT_SITE_URL,
      contactEmail: undefined,
      googleClientId: undefined,
      hstsEnabled: true,
      noindex: false,
      basemap: {
        mode: "vector",
        vendored: true,
        light: DEFAULT_STYLE_URL,
        dark: DEFAULT_STYLE_URL_DARK,
        attribution: DEFAULT_BASEMAP_ATTRIBUTION,
      },
    });
  });

  it("reads the plain names", () => {
    const config = readRuntimeConfig({
      SITE_URL: "https://dives.example.com",
      CONTACT_EMAIL: "hello@example.com",
      GOOGLE_CLIENT_ID: "client-id.apps.googleusercontent.com",
      MAP_TILE_URL: "https://tiles.example/{z}/{x}/{y}.png",
      MAP_ATTRIBUTION: "© Someone",
    });

    expect(config.siteUrl).toBe("https://dives.example.com");
    expect(config.contactEmail).toBe("hello@example.com");
    expect(config.googleClientId).toBe("client-id.apps.googleusercontent.com");
    expect(config.basemap.light).toBe("https://tiles.example/{z}/{x}/{y}.png");
    expect(config.basemap.attribution).toBe("© Someone");
  });

  // The one part of the tile configuration that is a credential, so it is its
  // own variable rather than something pasted into both URL templates.
  it("substitutes MAP_TILE_API_KEY into both templates", () => {
    const config = readRuntimeConfig({
      MAP_TILE_URL: "https://tiles.example/light/{z}/{x}/{y}.png?key={key}",
      MAP_TILE_URL_DARK: "https://tiles.example/dark/{z}/{x}/{y}.png?key={key}",
      MAP_TILE_API_KEY: "s3cret",
    });

    expect(config.basemap.light).toBe(
      "https://tiles.example/light/{z}/{x}/{y}.png?key=s3cret",
    );
    expect(config.basemap.dark).toBe(
      "https://tiles.example/dark/{z}/{x}/{y}.png?key=s3cret",
    );
  });

  // The whole point of the fallback: a deployment that configured the old,
  // build-time names keeps working after this module took the reading over.
  it("falls back to the NEXT_PUBLIC_ name", () => {
    const config = readRuntimeConfig({
      NEXT_PUBLIC_SITE_URL: "https://old.example.com",
      NEXT_PUBLIC_CONTACT_EMAIL: "hello@example.com",
      NEXT_PUBLIC_GOOGLE_CLIENT_ID: "old-client-id",
      NEXT_PUBLIC_MAP_TILE_URL: "https://old-tiles.example/{z}/{x}/{y}.png",
    });

    expect(config.siteUrl).toBe("https://old.example.com");
    expect(config.contactEmail).toBe("hello@example.com");
    expect(config.googleClientId).toBe("old-client-id");
    expect(config.basemap.light).toBe(
      "https://old-tiles.example/{z}/{x}/{y}.png",
    );
  });

  it("prefers the plain name over the NEXT_PUBLIC_ one", () => {
    const config = readRuntimeConfig({
      SITE_URL: "https://new.example.com",
      NEXT_PUBLIC_SITE_URL: "https://old.example.com",
    });

    expect(config.siteUrl).toBe("https://new.example.com");
  });

  // A variable set to nothing in a `.env` file is somebody who deleted a value,
  // not somebody who configured an empty string.
  it("treats a blank value as unset", () => {
    const config = readRuntimeConfig({
      CONTACT_EMAIL: "   ",
      GOOGLE_CLIENT_ID: "",
      NEXT_PUBLIC_GOOGLE_CLIENT_ID: "fallback-client-id",
    });

    expect(config.contactEmail).toBeUndefined();
    expect(config.googleClientId).toBe("fallback-client-id");
  });

  it("trims a value that a quote or a stray space crept into", () => {
    expect(
      readRuntimeConfig({ CONTACT_EMAIL: " hi@example.com " }).contactEmail,
    ).toBe("hi@example.com");
  });

  describe("WEB_HSTS and WEB_NOINDEX", () => {
    it.each(["0", "false", "no", "off"])("WEB_HSTS=%s turns HSTS off", (v) => {
      expect(readRuntimeConfig({ WEB_HSTS: v }).hstsEnabled).toBe(false);
    });

    it.each(["1", "true", "yes", "on"])(
      "WEB_NOINDEX=%s closes the site",
      (v) => {
        expect(readRuntimeConfig({ WEB_NOINDEX: v }).noindex).toBe(true);
      },
    );

    // Reading an unrecognized value as either answer would be a guess at what
    // somebody meant, and the guess is invisible either way - a site quietly left
    // open to crawlers looks exactly like one deliberately left open.
    it("keeps the default and says so for anything else", () => {
      const warn = vi.spyOn(console, "warn").mockImplementation(() => {});

      expect(readRuntimeConfig({ WEB_NOINDEX: "enabled" }).noindex).toBe(false);
      expect(warn).toHaveBeenCalledWith(
        expect.stringContaining("WEB_NOINDEX=enabled"),
      );
    });

    // Both are new names, so there is no build-time spelling of them to honour - and
    // offering one would invite an operator to set a variable that reaches the browser
    // for a decision only the server makes.
    it("ignores a NEXT_PUBLIC_ spelling of either", () => {
      const config = readRuntimeConfig({
        NEXT_PUBLIC_WEB_HSTS: "off",
        NEXT_PUBLIC_WEB_NOINDEX: "true",
      });

      expect(config.hstsEnabled).toBe(true);
      expect(config.noindex).toBe(false);
    });
  });

  describe("SITE_URL", () => {
    // `metadataBase` is a `new URL(...)` built in the root layout, so a value that
    // throws would take every page down over its metadata.
    it("falls back and warns rather than letting the layout throw", () => {
      const warn = vi.spyOn(console, "warn").mockImplementation(() => {});

      expect(readRuntimeConfig({ SITE_URL: "dives.example.com" }).siteUrl).toBe(
        DEFAULT_SITE_URL,
      );
      expect(warn).toHaveBeenCalledWith(
        expect.stringContaining("dives.example.com"),
      );
    });

    // "htp://" parses happily as a non-special scheme, so nothing throws - it just
    // reaches crawlers as a canonical URL nobody can fetch.
    it("rejects a mistyped scheme", () => {
      vi.spyOn(console, "warn").mockImplementation(() => {});

      expect(
        readRuntimeConfig({ SITE_URL: "htp://dives.example.com" }).siteUrl,
      ).toBe(DEFAULT_SITE_URL);
    });

    it("keeps a port and a scheme it can serve from", () => {
      expect(
        readRuntimeConfig({ SITE_URL: "http://192.168.1.4:3000" }).siteUrl,
      ).toBe("http://192.168.1.4:3000");
    });
  });
});

// The refusal has to fire on exactly one of the three basemap states. Trip the
// default path with it and a stock configuration takes the whole app down at the
// first request, since `runtimeConfig()` is what every page and the middleware
// read - which is the shape that once killed the API repo's test collection and
// its compose stack together.
describe("the basemap's three states", () => {
  it("refuses to serve a style configured with no credit", () => {
    expect(() =>
      readRuntimeConfig({ MAP_STYLE_URL: "https://styles.example/day.json" }),
    ).toThrow(MISSING_ATTRIBUTION_MESSAGE);
  });

  it("serves a style configured with one", () => {
    const config = readRuntimeConfig({
      MAP_STYLE_URL: "https://styles.example/day.json",
      MAP_ATTRIBUTION: "© Someone",
    });
    expect(config.basemap).toMatchObject({
      mode: "vector",
      vendored: false,
      light: "https://styles.example/day.json",
      attribution: "© Someone",
    });
  });

  it("serves the raster escape hatch silently", () => {
    const config = readRuntimeConfig({
      MAP_TILE_URL: "https://tiles.example/{z}/{x}/{y}.png",
    });
    expect(config.basemap.mode).toBe("raster");
  });

  it("serves the bundled pair silently", () => {
    expect(readRuntimeConfig({}).basemap).toMatchObject({ vendored: true });
  });

  // One credit, applying to whichever basemap is active - which is why it lost
  // the `MAP_TILE_` prefix. An operator who configures a style has to be able to
  // credit it, and there is exactly one credit on screen at a time.
  it("reads one attribution for whichever mode is active", () => {
    expect(
      readRuntimeConfig({
        MAP_TILE_URL: "https://tiles.example/{z}/{x}/{y}.png",
        MAP_ATTRIBUTION: "© Someone",
      }).basemap.attribution,
    ).toBe("© Someone");
    expect(
      readRuntimeConfig({
        MAP_STYLE_URL: "https://styles.example/day.json",
        MAP_ATTRIBUTION: "© Someone",
      }).basemap.attribution,
    ).toBe("© Someone");
  });
});

describe("publicConfig", () => {
  // Not a filter over the whole config: a server-only value added later should have
  // to be listed here before it reaches anyone's browser.
  it("carries only what the browser is given", () => {
    const config = publicConfig(
      readRuntimeConfig({
        SITE_URL: "https://dives.example.com",
        CONTACT_EMAIL: "hello@example.com",
        GOOGLE_CLIENT_ID: "client-id",
      }),
    );

    expect(config).toEqual({
      googleClientId: "client-id",
      basemap: {
        mode: "vector",
        vendored: true,
        light: DEFAULT_STYLE_URL,
        dark: DEFAULT_STYLE_URL_DARK,
        attribution: DEFAULT_BASEMAP_ATTRIBUTION,
      },
    });
  });
});