393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { envSchema, validateEnv } from "./index.js";
|
|
|
|
/** All required env vars (no defaults/optional) */
|
|
const REQUIRED_ENV = {
|
|
DATABASE_URL: "postgresql://user:pass@localhost:5432/sase",
|
|
REDIS_PASSWORD: "redis-secret-password",
|
|
BETTER_AUTH_SECRET: "x".repeat(32),
|
|
BETTER_AUTH_URL: "http://localhost:4000",
|
|
MINIO_ENDPOINT: "http://localhost:9000",
|
|
MINIO_ACCESS_KEY: "minio-access",
|
|
MINIO_SECRET_KEY: "minio-secret",
|
|
MINIO_PUBLIC_URL: "http://localhost:9000/sase-schemas",
|
|
};
|
|
|
|
/** Full valid env object */
|
|
function validEnv() {
|
|
return { ...REQUIRED_ENV };
|
|
}
|
|
|
|
describe("envSchema", () => {
|
|
describe("happy path", () => {
|
|
it("parses minimal valid env (required only)", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.DATABASE_URL).toBe(REQUIRED_ENV.DATABASE_URL);
|
|
expect(result.data.BETTER_AUTH_SECRET).toBe(REQUIRED_ENV.BETTER_AUTH_SECRET);
|
|
}
|
|
});
|
|
|
|
it("parses full env with all optional fields set", () => {
|
|
const env = {
|
|
...REQUIRED_ENV,
|
|
NODE_ENV: "production",
|
|
GOOGLE_CLIENT_ID: "google-client-id",
|
|
GOOGLE_CLIENT_SECRET: "google-client-secret",
|
|
STRIPE_SECRET_KEY: "sk_test_...",
|
|
STRIPE_PUBLISHABLE_KEY: "pk_test_...",
|
|
STRIPE_WEBHOOK_SECRET: "whsec_...",
|
|
PL24_BASE_URL: "https://pl24.example.com",
|
|
PL24_COMPANY_CODE: "tr-903645",
|
|
PL24_USERNAME: "pl24-user",
|
|
PL24_PASSWORD: "pl24-pass",
|
|
PL24_COMPANY_CODE_2: "de-708171",
|
|
PL24_USERNAME_2: "pl24-user-2",
|
|
PL24_PASSWORD_2: "pl24-pass-2",
|
|
PL24_PROXY_DE: "http://user:pass@gw.dataimpulse.com:10000",
|
|
EMEX_USERNAME: "emex-user",
|
|
EMEX_PASSWORD: "emex-pass",
|
|
PCAT_PROXY_USER: "pcat-user",
|
|
PCAT_PROXY_PASS: "pcat-pass",
|
|
OPENROUTER_API_KEY: "sk-or-...",
|
|
POSTAL_API_URL: "https://postal.example.com",
|
|
POSTAL_API_KEY: "postal-key",
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",
|
|
OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=abc",
|
|
POSTHOG_API_KEY: "phc_...",
|
|
SENTRY_DSN: "https://abc@sentr.ing/123",
|
|
CHANGELOG_AUTOMATION_TOKEN: "y".repeat(32),
|
|
};
|
|
const result = envSchema.safeParse(env);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.NODE_ENV).toBe("production");
|
|
expect(result.data.GOOGLE_CLIENT_ID).toBe("google-client-id");
|
|
expect(result.data.STRIPE_SECRET_KEY).toBe("sk_test_...");
|
|
expect(result.data.CHANGELOG_AUTOMATION_TOKEN).toBe("y".repeat(32));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("missing required fields", () => {
|
|
const requiredKeys = Object.keys(REQUIRED_ENV) as (keyof typeof REQUIRED_ENV)[];
|
|
|
|
for (const key of requiredKeys) {
|
|
it(`throws when ${key} is missing`, () => {
|
|
const env = { ...REQUIRED_ENV };
|
|
delete env[key];
|
|
const result = envSchema.safeParse(env);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("invalid types", () => {
|
|
it("rejects non-URL DATABASE_URL", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, DATABASE_URL: "not-a-url" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects non-URL BETTER_AUTH_URL", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, BETTER_AUTH_URL: "not-a-url" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects too-short BETTER_AUTH_SECRET", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, BETTER_AUTH_SECRET: "short" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects out-of-range OTEL_TRACE_SAMPLE_RATE", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, OTEL_TRACE_SAMPLE_RATE: "1.5" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects invalid NODE_ENV", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, NODE_ENV: "staging" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("coercion and transforms", () => {
|
|
it("coerces PORT string to number", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, PORT: "8080" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.PORT).toBe(8080);
|
|
expect(typeof result.data.PORT).toBe("number");
|
|
}
|
|
});
|
|
|
|
it("coerces REDIS_PORT string to number", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, REDIS_PORT: "6380" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.REDIS_PORT).toBe(6380);
|
|
}
|
|
});
|
|
|
|
it("transforms MINIO_USE_SSL=true string to boolean true", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, MINIO_USE_SSL: "true" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.MINIO_USE_SSL).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("transforms MINIO_USE_SSL=false string to boolean false", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, MINIO_USE_SSL: "false" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.MINIO_USE_SSL).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("transforms OTEL_ENABLED=true string to boolean true", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, OTEL_ENABLED: "true" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.OTEL_ENABLED).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("transforms OTEL_ENABLED=false string to boolean false", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, OTEL_ENABLED: "false" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.OTEL_ENABLED).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("transforms ML_PREDICTION_ENABLED=true string to boolean true", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, ML_PREDICTION_ENABLED: "true" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.ML_PREDICTION_ENABLED).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("coerces OTEL_TRACE_SAMPLE_RATE string to number", () => {
|
|
const result = envSchema.safeParse({ ...REQUIRED_ENV, OTEL_TRACE_SAMPLE_RATE: "0.5" });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.OTEL_TRACE_SAMPLE_RATE).toBe(0.5);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("defaults", () => {
|
|
it("defaults NODE_ENV to development", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.NODE_ENV).toBe("development");
|
|
}
|
|
});
|
|
|
|
it("defaults PORT to 4000", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.PORT).toBe(4000);
|
|
}
|
|
});
|
|
|
|
it("defaults REDIS_HOST to 127.0.0.1", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.REDIS_HOST).toBe("127.0.0.1");
|
|
}
|
|
});
|
|
|
|
it("defaults REDIS_PORT to 6379", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.REDIS_PORT).toBe(6379);
|
|
}
|
|
});
|
|
|
|
it("defaults MINIO_BUCKET_NAME to sase-schemas", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.MINIO_BUCKET_NAME).toBe("sase-schemas");
|
|
}
|
|
});
|
|
|
|
it("defaults MINIO_USE_SSL to false", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.MINIO_USE_SSL).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("defaults CORS_ORIGIN to localhost:3000", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.CORS_ORIGIN).toBe("http://localhost:3000");
|
|
}
|
|
});
|
|
|
|
it("defaults STRIPE_SUCCESS_URL", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.STRIPE_SUCCESS_URL).toContain("stripe=success");
|
|
}
|
|
});
|
|
|
|
it("defaults STRIPE_CANCEL_URL", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.STRIPE_CANCEL_URL).toContain("stripe=cancelled");
|
|
}
|
|
});
|
|
|
|
it("defaults PCAT_USE_PROXY to true", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.PCAT_USE_PROXY).toBe("true");
|
|
}
|
|
});
|
|
|
|
it("defaults PCAT_PROXY_HOST to gw.dataimpulse.com", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.PCAT_PROXY_HOST).toBe("gw.dataimpulse.com");
|
|
}
|
|
});
|
|
|
|
it("defaults ML_PREDICTION_ENABLED to false", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.ML_PREDICTION_ENABLED).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("defaults POSTAL_FROM_ADDRESS to noreply@sase.tr", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.POSTAL_FROM_ADDRESS).toBe("noreply@sase.tr");
|
|
}
|
|
});
|
|
|
|
it("defaults POSTAL_FROM_NAME to Sase.tr", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.POSTAL_FROM_NAME).toBe("Sase.tr");
|
|
}
|
|
});
|
|
|
|
it("defaults OTEL_ENABLED to false", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.OTEL_ENABLED).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("defaults OTEL_SERVICE_NAME to sase-api", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.OTEL_SERVICE_NAME).toBe("sase-api");
|
|
}
|
|
});
|
|
|
|
it("defaults OTEL_TRACE_SAMPLE_RATE to 1.0", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.OTEL_TRACE_SAMPLE_RATE).toBe(1.0);
|
|
}
|
|
});
|
|
|
|
it("defaults POSTHOG_HOST to https://t.sase.tr", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.POSTHOG_HOST).toBe("https://t.sase.tr");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("optional fields", () => {
|
|
it("returns undefined for GOOGLE_CLIENT_ID when unset", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.GOOGLE_CLIENT_ID).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("returns undefined for STRIPE_SECRET_KEY when unset", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.STRIPE_SECRET_KEY).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("returns undefined for SENTRY_DSN when unset", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.SENTRY_DSN).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("returns undefined for CHANGELOG_AUTOMATION_TOKEN when unset", () => {
|
|
const result = envSchema.safeParse(REQUIRED_ENV);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.CHANGELOG_AUTOMATION_TOKEN).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("validateEnv", () => {
|
|
const OLD_ENV = { ...process.env };
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...REQUIRED_ENV };
|
|
});
|
|
|
|
afterAll(() => {
|
|
process.env = OLD_ENV;
|
|
});
|
|
|
|
it("returns parsed config on success", () => {
|
|
const config = validateEnv();
|
|
expect(config.DATABASE_URL).toBe(REQUIRED_ENV.DATABASE_URL);
|
|
});
|
|
|
|
it("throws Error when required field is missing", () => {
|
|
delete process.env.DATABASE_URL;
|
|
expect(() => validateEnv()).toThrow("Invalid environment variables");
|
|
});
|
|
|
|
it("accepts explicit env parameter instead of process.env", () => {
|
|
const customEnv = { ...REQUIRED_ENV, PORT: "9999" };
|
|
const config = validateEnv(customEnv);
|
|
expect(config.PORT).toBe(9999);
|
|
});
|
|
|
|
it("accepts all required env vars as env parameter", () => {
|
|
const config = validateEnv(REQUIRED_ENV);
|
|
expect(config.DATABASE_URL).toBe(REQUIRED_ENV.DATABASE_URL);
|
|
});
|
|
});
|