src/lib/runtime-config.test.ts
Rama main · réplica de lectura del repositorio original.
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,
},
});
});
});