import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; // Ensure SESSION_SECRET is always set for all config tests BEFORE capturing originalEnv process.env.SESSION_SECRET = "this-is-a-valid-32-char-secret!!"; describe("loadConfig", () => { // Now capture originalEnv AFTER setting SESSION_SECRET const originalEnv = { ...process.env }; beforeEach(() => { vi.resetModules(); }); afterEach(() => { process.env = { ...originalEnv }; }); async function loadConfig() { const mod = await import("../config.js"); return mod.loadConfig(); } it("returns default port 3000 when PORT is undefined", async () => { delete process.env.PORT; const config = await loadConfig(); expect(config.port).toBe(3000); }); it("parses PORT as an integer", async () => { process.env.PORT = "4000"; const config = await loadConfig(); expect(config.port).toBe(4000); expect(typeof config.port).toBe("number"); }); it("returns default PDS URL when PDS_URL is undefined", async () => { delete process.env.PDS_URL; const config = await loadConfig(); expect(config.pdsUrl).toBe("https://bsky.social"); }); it("uses provided environment variables", async () => { process.env.PORT = "5000"; process.env.FORUM_DID = "did:plc:test123"; process.env.PDS_URL = "https://my-pds.example.com"; process.env.DATABASE_URL = "postgres://localhost/testdb"; const config = await loadConfig(); expect(config.port).toBe(5000); expect(config.forumDid).toBe("did:plc:test123"); expect(config.pdsUrl).toBe("https://my-pds.example.com"); expect(config.databaseUrl).toBe("postgres://localhost/testdb"); }); it("returns empty string for forumDid when FORUM_DID is undefined", async () => { delete process.env.FORUM_DID; const config = await loadConfig(); expect(config.forumDid).toBe(""); }); it("returns empty string for databaseUrl when DATABASE_URL is undefined", async () => { delete process.env.DATABASE_URL; const config = await loadConfig(); expect(config.databaseUrl).toBe(""); }); it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => { process.env.PORT = ""; const config = await loadConfig(); // Documents a gap: ?? only catches null/undefined, not "" expect(config.port).toBeNaN(); }); describe("OAuth configuration", () => { it("loads OAuth configuration from environment variables", async () => { process.env.OAUTH_PUBLIC_URL = "https://forum.example.com"; process.env.SESSION_SECRET = "my-super-secret-key-that-is-32-chars"; process.env.SESSION_TTL_DAYS = "14"; process.env.REDIS_URL = "redis://localhost:6379"; const config = await loadConfig(); expect(config.oauthPublicUrl).toBe("https://forum.example.com"); expect(config.sessionSecret).toBe("my-super-secret-key-that-is-32-chars"); expect(config.sessionTtlDays).toBe(14); expect(config.redisUrl).toBe("redis://localhost:6379"); }); it("uses default values for optional OAuth config", async () => { delete process.env.OAUTH_PUBLIC_URL; delete process.env.SESSION_TTL_DAYS; delete process.env.REDIS_URL; const config = await loadConfig(); expect(config.oauthPublicUrl).toBe("http://localhost:3000"); expect(config.sessionTtlDays).toBe(7); expect(config.redisUrl).toBeUndefined(); }); it("throws error when SESSION_SECRET is missing", async () => { delete process.env.SESSION_SECRET; await expect(loadConfig()).rejects.toThrow( "SESSION_SECRET must be at least 32 characters" ); }); it("throws error when SESSION_SECRET is too short", async () => { process.env.SESSION_SECRET = "too-short"; await expect(loadConfig()).rejects.toThrow( "SESSION_SECRET must be at least 32 characters" ); }); it("accepts SESSION_SECRET with exactly 32 characters", async () => { process.env.SESSION_SECRET = "12345678901234567890123456789012"; // exactly 32 chars const config = await loadConfig(); expect(config.sessionSecret).toBe("12345678901234567890123456789012"); }); it("throws error when OAUTH_PUBLIC_URL is missing in production", async () => { process.env.NODE_ENV = "production"; delete process.env.OAUTH_PUBLIC_URL; await expect(loadConfig()).rejects.toThrow( "OAUTH_PUBLIC_URL is required in production" ); }); it("allows missing OAUTH_PUBLIC_URL in development", async () => { delete process.env.NODE_ENV; delete process.env.OAUTH_PUBLIC_URL; const config = await loadConfig(); expect(config.oauthPublicUrl).toBe("http://localhost:3000"); }); it("warns about in-memory sessions in production", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); process.env.NODE_ENV = "production"; process.env.OAUTH_PUBLIC_URL = "https://example.com"; delete process.env.REDIS_URL; await loadConfig(); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("in-memory session storage in production") ); warnSpy.mockRestore(); }); it("does not warn about in-memory sessions when REDIS_URL is set", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); process.env.NODE_ENV = "production"; process.env.OAUTH_PUBLIC_URL = "https://example.com"; process.env.REDIS_URL = "redis://localhost:6379"; await loadConfig(); expect(warnSpy).not.toHaveBeenCalled(); warnSpy.mockRestore(); }); it("does not warn about in-memory sessions in development", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); delete process.env.NODE_ENV; delete process.env.REDIS_URL; await loadConfig(); expect(warnSpy).not.toHaveBeenCalledWith( expect.stringContaining("in-memory session storage") ); warnSpy.mockRestore(); }); }); describe("Forum credentials", () => { it("loads forum credentials from environment", async () => { process.env.FORUM_HANDLE = "forum.example.com"; process.env.FORUM_PASSWORD = "test-password"; const config = await loadConfig(); expect(config.forumHandle).toBe("forum.example.com"); expect(config.forumPassword).toBe("test-password"); }); it("allows missing forum credentials (optional)", async () => { delete process.env.FORUM_HANDLE; delete process.env.FORUM_PASSWORD; const config = await loadConfig(); expect(config.forumHandle).toBeUndefined(); expect(config.forumPassword).toBeUndefined(); }); }); describe("Backfill configuration", () => { it("uses default backfill values when env vars not set", async () => { delete process.env.BACKFILL_RATE_LIMIT; delete process.env.BACKFILL_CONCURRENCY; delete process.env.BACKFILL_CURSOR_MAX_AGE_HOURS; const config = await loadConfig(); expect(config.backfillRateLimit).toBe(10); expect(config.backfillConcurrency).toBe(10); expect(config.backfillCursorMaxAgeHours).toBe(48); }); it("reads backfill values from env vars", async () => { process.env.BACKFILL_RATE_LIMIT = "5"; process.env.BACKFILL_CONCURRENCY = "20"; process.env.BACKFILL_CURSOR_MAX_AGE_HOURS = "24"; const config = await loadConfig(); expect(config.backfillRateLimit).toBe(5); expect(config.backfillConcurrency).toBe(20); expect(config.backfillCursorMaxAgeHours).toBe(24); }); }); });