From 1610aa1008432d15464144cb4296d144f927f233 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 31 Aug 2026 21:41:40 +0200 Subject: [PATCH] fix: keep server env out of avatar client bundle --- src/lib/format.test.ts | 19 ++++++------------- src/lib/imager-client.test.ts | 29 +++++++++++++++++++++++++++++ src/lib/imager.test.ts | 27 ++++++++++----------------- src/lib/imager.ts | 3 +-- 4 files changed, 46 insertions(+), 32 deletions(-) create mode 100644 src/lib/imager-client.test.ts diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 6295790d..63c9043f 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -1,17 +1,10 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const envMock = vi.hoisted(() => ({ - env: { - NEXT_PUBLIC_IMAGER_URL: "https://img.example.com" as string | undefined, - NEXT_PUBLIC_APP_URL: undefined as string | undefined, - IMAGING_UPSTREAM_URL: "https://up.example.com" as string | undefined, - }, -})); - -vi.mock("@/env", () => envMock); - +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { avatarImageUrl, excerpt, slugify } from "./format"; +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe("slugify", () => { it("lowercases and hyphenates", () => { expect(slugify("Hello World!")).toBe("hello-world"); @@ -26,7 +19,7 @@ describe("slugify", () => { describe("avatarImageUrl", () => { beforeEach(() => { - envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com"; + vi.stubEnv("NEXT_PUBLIC_IMAGER_URL", "https://img.example.com"); }); it("appends the figure and options", () => { diff --git a/src/lib/imager-client.test.ts b/src/lib/imager-client.test.ts new file mode 100644 index 00000000..f0b535b2 --- /dev/null +++ b/src/lib/imager-client.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const originalDatabaseUrl = process.env.DATABASE_URL; +const originalHotelName = process.env.HOTEL_NAME; +const originalSkipValidation = process.env.SKIP_ENV_VALIDATION; + +afterEach(() => { + vi.resetModules(); + if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL; + else process.env.DATABASE_URL = originalDatabaseUrl; + if (originalHotelName === undefined) delete process.env.HOTEL_NAME; + else process.env.HOTEL_NAME = originalHotelName; + if (originalSkipValidation === undefined) + delete process.env.SKIP_ENV_VALIDATION; + else process.env.SKIP_ENV_VALIDATION = originalSkipValidation; +}); + +describe("imager client boundary", () => { + it("loads without server-only environment variables", async () => { + delete process.env.DATABASE_URL; + delete process.env.HOTEL_NAME; + vi.resetModules(); + delete process.env.SKIP_ENV_VALIDATION; + + const imager = await import("./imager"); + + expect(imager.getAvatarUrl("hd-180-1")).toContain("figure=hd-180-1"); + }); +}); diff --git a/src/lib/imager.test.ts b/src/lib/imager.test.ts index f6d9e53e..ab9a1de0 100644 --- a/src/lib/imager.test.ts +++ b/src/lib/imager.test.ts @@ -1,20 +1,13 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const envMock = vi.hoisted(() => ({ - env: { - NEXT_PUBLIC_IMAGER_URL: "https://img.example.com" as string | undefined, - NEXT_PUBLIC_APP_URL: undefined as string | undefined, - IMAGING_UPSTREAM_URL: "https://up.example.com" as string | undefined, - }, -})); - -vi.mock("@/env", () => envMock); - +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getAvatarUrl, resolveImagerBase } from "./imager"; +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe("resolveImagerBase", () => { beforeEach(() => { - envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com"; + vi.stubEnv("NEXT_PUBLIC_IMAGER_URL", "https://img.example.com"); }); it("returns configured imager env var", () => { @@ -22,12 +15,12 @@ describe("resolveImagerBase", () => { }); it("strips trailing slashes", () => { - envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com/"; + vi.stubEnv("NEXT_PUBLIC_IMAGER_URL", "https://img.example.com/"); expect(resolveImagerBase()).toBe("https://img.example.com"); }); it("falls back to epicnabbo.nl when env var is not set", () => { - envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined; + vi.stubEnv("NEXT_PUBLIC_IMAGER_URL", ""); expect(resolveImagerBase()).toBe( "https://epicnabbo.nl/imaging/avatarimage", ); @@ -36,7 +29,7 @@ describe("resolveImagerBase", () => { describe("getAvatarUrl", () => { beforeEach(() => { - envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com"; + vi.stubEnv("NEXT_PUBLIC_IMAGER_URL", "https://img.example.com"); }); it("builds a query string with defaults (omits default params)", () => { @@ -83,7 +76,7 @@ describe("getAvatarUrl", () => { }); it("uses epicnabbo.nl when no env var is set", () => { - envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined; + vi.stubEnv("NEXT_PUBLIC_IMAGER_URL", ""); const url = getAvatarUrl("hd-180-1"); expect(url.startsWith("https://epicnabbo.nl/imaging/avatarimage?")).toBe( true, diff --git a/src/lib/imager.ts b/src/lib/imager.ts index 56297d94..861cfe0f 100644 --- a/src/lib/imager.ts +++ b/src/lib/imager.ts @@ -7,7 +7,6 @@ export type { AvatarOptions } from "@/types/admin"; -import { env } from "@/env"; import type { AvatarOptions } from "@/types/admin"; const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage"; @@ -17,7 +16,7 @@ const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage"; * Falls back to epicnabbo.nl if not configured. */ export function resolveImagerBase(): string { - const fromEnv = env.NEXT_PUBLIC_IMAGER_URL?.trim(); + const fromEnv = process.env.NEXT_PUBLIC_IMAGER_URL?.trim(); if (!fromEnv) { return DEFAULT_IMAGER_URL; }