import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; describe("lib/env boot assertion", () => { beforeEach(() => { // Re-evaluate the module per case so its top-level assertion re-runs. vi.resetModules(); }); afterEach(() => { vi.unstubAllEnvs(); }); it("throws a clear error when a required env var is missing", async () => { // Given - UI_API_BASE_URL and its legacy empty, the others present vi.stubEnv("UI_API_BASE_URL", ""); vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", ""); vi.stubEnv("AUTH_URL", "http://localhost:3000"); vi.stubEnv("AUTH_SECRET", "secret"); // When / Then await expect(import("@/lib/env")).rejects.toThrow( "Missing required env: UI_API_BASE_URL", ); }); it("does not throw when every required env var is present", async () => { // Given vi.stubEnv("UI_API_BASE_URL", "https://api.example.com/api/v1"); vi.stubEnv("AUTH_URL", "http://localhost:3000"); vi.stubEnv("AUTH_SECRET", "secret"); // When / Then await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("accepts the deprecated NEXT_PUBLIC_API_BASE_URL as a fallback", async () => { // Given - the new name is unset but the legacy name is configured vi.stubEnv("UI_API_BASE_URL", undefined); vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.com/api/v1"); vi.stubEnv("AUTH_URL", "http://localhost:3000"); vi.stubEnv("AUTH_SECRET", "secret"); // When / Then await expect(import("@/lib/env")).resolves.toBeDefined(); }); }); describe("lib/env gated integration validation", () => { // Clear every gated flag/config so ambient shell env cannot affect assertions, // then satisfy the unconditional REQUIRED vars (they are checked first). const GATED_ENV_VARS = [ "UI_SENTRY_ENABLED", "UI_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN", "UI_SENTRY_ENVIRONMENT", "NEXT_PUBLIC_SENTRY_ENVIRONMENT", "UI_GOOGLE_TAG_MANAGER_ENABLED", "UI_GOOGLE_TAG_MANAGER_ID", "NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID", "UI_POSTHOG_ENABLED", "UI_POSTHOG_KEY", "POSTHOG_KEY", "UI_POSTHOG_HOST", "POSTHOG_HOST", "CLOUD_BILLING_ENABLED", ] as const; beforeEach(() => { vi.resetModules(); for (const key of GATED_ENV_VARS) { vi.stubEnv(key, undefined); } vi.stubEnv("UI_API_BASE_URL", "https://api.example.com/api/v1"); vi.stubEnv("AUTH_URL", "http://localhost:3000"); vi.stubEnv("AUTH_SECRET", "secret"); vi.spyOn(console, "warn").mockImplementation(() => {}); }); afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); }); it("throws when UI_SENTRY_ENABLED is true but the DSN is unset", async () => { vi.stubEnv("UI_SENTRY_ENABLED", "true"); await expect(import("@/lib/env")).rejects.toThrow("UI_SENTRY_DSN"); }); it("resolves when UI_SENTRY_ENABLED is true and the DSN is present", async () => { vi.stubEnv("UI_SENTRY_ENABLED", "true"); vi.stubEnv("UI_SENTRY_DSN", "https://key@o0.ingest.sentry.io/1"); await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("does not throw when an integration is disabled and its config is unset", async () => { // No enable flags set — the highest-value non-regression: a default // deployment that never opted into any integration must still boot. await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("throws when UI_GOOGLE_TAG_MANAGER_ENABLED is true without an id", async () => { vi.stubEnv("UI_GOOGLE_TAG_MANAGER_ENABLED", "true"); await expect(import("@/lib/env")).rejects.toThrow( "UI_GOOGLE_TAG_MANAGER_ID", ); }); it("throws on UI_POSTHOG_HOST when PostHog is enabled with only the key", async () => { vi.stubEnv("UI_POSTHOG_ENABLED", "true"); vi.stubEnv("UI_POSTHOG_KEY", "phc_key"); await expect(import("@/lib/env")).rejects.toThrow("UI_POSTHOG_HOST"); }); it("resolves when PostHog is enabled with both key and host", async () => { vi.stubEnv("UI_POSTHOG_ENABLED", "true"); vi.stubEnv("UI_POSTHOG_KEY", "phc_key"); vi.stubEnv("UI_POSTHOG_HOST", "https://eu.i.posthog.com"); await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("resolves when the legacy Sentry DSN is set without the enable flag", async () => { // Legacy names stay backward compatible: they activate without the flag. vi.stubEnv("NEXT_PUBLIC_SENTRY_DSN", "https://key@o0.ingest.sentry.io/1"); await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("resolves when the legacy PostHog names are set without the enable flag", async () => { vi.stubEnv("POSTHOG_KEY", "phc_key"); vi.stubEnv("POSTHOG_HOST", "https://eu.i.posthog.com"); await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("throws on a partial legacy PostHog config set without the enable flag", async () => { vi.stubEnv("POSTHOG_KEY", "phc_key"); await expect(import("@/lib/env")).rejects.toThrow("POSTHOG_HOST"); }); it("throws when CLOUD_BILLING_ENABLED is metronome but PostHog is not enabled", async () => { // metronome billing routes per tenant via the BILLING_SYSTEM_METRONOME // PostHog flag, so PostHog must be enabled. vi.stubEnv("CLOUD_BILLING_ENABLED", "metronome"); await expect(import("@/lib/env")).rejects.toThrow( "PostHog is required for per-tenant billing routing", ); }); it("resolves when CLOUD_BILLING_ENABLED is metronome and PostHog is enabled", async () => { vi.stubEnv("CLOUD_BILLING_ENABLED", "metronome"); vi.stubEnv("UI_POSTHOG_ENABLED", "true"); vi.stubEnv("UI_POSTHOG_KEY", "phc_key"); vi.stubEnv("UI_POSTHOG_HOST", "https://eu.i.posthog.com"); await expect(import("@/lib/env")).resolves.toBeDefined(); }); it("resolves when CLOUD_BILLING_ENABLED is legacy without PostHog", async () => { // legacy billing forces V1 and never touches PostHog. vi.stubEnv("CLOUD_BILLING_ENABLED", "legacy"); await expect(import("@/lib/env")).resolves.toBeDefined(); }); });