diff --git a/apps/web/lib/env.test.ts b/apps/web/lib/env.test.ts new file mode 100644 index 0000000..428663e --- /dev/null +++ b/apps/web/lib/env.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +async function loadEnv() { + vi.resetModules(); + return (await import("./env")).env; +} + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +describe("env numeric configuration", () => { + it("loads valid positive integer limits and USD prices", async () => { + vi.stubEnv("MAX_IMAGE_UPLOAD_MB", "24"); + vi.stubEnv("MAX_VIDEO_DURATION_SECONDS", "45"); + vi.stubEnv("PRICE_API_ACCESS_USD", "1.50"); + + const env = await loadEnv(); + + expect(env.maxImageUploadMb).toBe(24); + expect(env.maxVideoDurationSeconds).toBe(45); + expect(env.priceApiAccessUsd).toBe(1.5); + }); + + it("falls back for malformed or non-positive limits", async () => { + vi.stubEnv("MAX_IMAGE_UPLOAD_MB", "10abc"); + vi.stubEnv("MAX_VIDEO_UPLOAD_MB", "0"); + vi.stubEnv("RSS_CACHE_SECONDS", "1.5"); + + const env = await loadEnv(); + + expect(env.maxImageUploadMb).toBe(12); + expect(env.maxVideoUploadMb).toBe(75); + expect(env.rssCacheSeconds).toBe(300); + }); + + it("falls back for invalid checkout prices", async () => { + vi.stubEnv("PRICE_API_ACCESS_USD", "0"); + vi.stubEnv("PRICE_LIFETIME_USD", "-2"); + vi.stubEnv("PRICE_PLAY_PASS_USD", "Infinity"); + + const env = await loadEnv(); + + expect(env.priceApiAccessUsd).toBe(1); + expect(env.priceLifetimeUsd).toBe(2); + expect(env.pricePlayPassUsd).toBe(1); + }); +}); diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 049ad6c..bcc0bbb 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -8,8 +8,17 @@ function bool(v: string | undefined, def = false): boolean { } function int(v: string | undefined, def: number): number { + if (v == null || v.trim() === "") return def; + if (!/^\d+$/.test(v.trim())) return def; const n = Number(v); - return Number.isFinite(n) ? n : def; + return Number.isSafeInteger(n) && n > 0 ? n : def; +} + +function usd(v: string | undefined, def: number): number { + if (v == null || v.trim() === "") return def; + if (!/^\d+(?:\.\d{1,2})?$/.test(v.trim())) return def; + const n = Number(v); + return Number.isFinite(n) && n > 0 ? n : def; } export const env = { @@ -61,9 +70,9 @@ export const env = { get coinpayConfigured() { return !!(this.coinpay.apiKey && this.coinpay.businessId); }, - priceApiAccessUsd: Number(process.env.PRICE_API_ACCESS_USD || 1), - priceLifetimeUsd: Number(process.env.PRICE_LIFETIME_USD || 2), - pricePlayPassUsd: Number(process.env.PRICE_PLAY_PASS_USD || 1), + priceApiAccessUsd: usd(process.env.PRICE_API_ACCESS_USD, 1), + priceLifetimeUsd: usd(process.env.PRICE_LIFETIME_USD, 2), + pricePlayPassUsd: usd(process.env.PRICE_PLAY_PASS_USD, 1), cronSecret: process.env.CRON_SECRET || "", vapid: {