WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 232 lines 7.7 kB view raw
1import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 3// Ensure SESSION_SECRET is always set for all config tests BEFORE capturing originalEnv 4process.env.SESSION_SECRET = "this-is-a-valid-32-char-secret!!"; 5 6describe("loadConfig", () => { 7 // Now capture originalEnv AFTER setting SESSION_SECRET 8 const originalEnv = { ...process.env }; 9 10 beforeEach(() => { 11 vi.resetModules(); 12 }); 13 14 afterEach(() => { 15 process.env = { ...originalEnv }; 16 }); 17 18 async function loadConfig() { 19 const mod = await import("../config.js"); 20 return mod.loadConfig(); 21 } 22 23 it("returns default port 3000 when PORT is undefined", async () => { 24 delete process.env.PORT; 25 const config = await loadConfig(); 26 expect(config.port).toBe(3000); 27 }); 28 29 it("parses PORT as an integer", async () => { 30 process.env.PORT = "4000"; 31 const config = await loadConfig(); 32 expect(config.port).toBe(4000); 33 expect(typeof config.port).toBe("number"); 34 }); 35 36 it("returns default PDS URL when PDS_URL is undefined", async () => { 37 delete process.env.PDS_URL; 38 const config = await loadConfig(); 39 expect(config.pdsUrl).toBe("https://bsky.social"); 40 }); 41 42 it("uses provided environment variables", async () => { 43 process.env.PORT = "5000"; 44 process.env.FORUM_DID = "did:plc:test123"; 45 process.env.PDS_URL = "https://my-pds.example.com"; 46 process.env.DATABASE_URL = "postgres://localhost/testdb"; 47 const config = await loadConfig(); 48 expect(config.port).toBe(5000); 49 expect(config.forumDid).toBe("did:plc:test123"); 50 expect(config.pdsUrl).toBe("https://my-pds.example.com"); 51 expect(config.databaseUrl).toBe("postgres://localhost/testdb"); 52 }); 53 54 it("returns empty string for forumDid when FORUM_DID is undefined", async () => { 55 delete process.env.FORUM_DID; 56 const config = await loadConfig(); 57 expect(config.forumDid).toBe(""); 58 }); 59 60 it("returns empty string for databaseUrl when DATABASE_URL is undefined", async () => { 61 delete process.env.DATABASE_URL; 62 const config = await loadConfig(); 63 expect(config.databaseUrl).toBe(""); 64 }); 65 66 it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => { 67 process.env.PORT = ""; 68 const config = await loadConfig(); 69 // Documents a gap: ?? only catches null/undefined, not "" 70 expect(config.port).toBeNaN(); 71 }); 72 73 describe("OAuth configuration", () => { 74 it("loads OAuth configuration from environment variables", async () => { 75 process.env.OAUTH_PUBLIC_URL = "https://forum.example.com"; 76 process.env.SESSION_SECRET = "my-super-secret-key-that-is-32-chars"; 77 process.env.SESSION_TTL_DAYS = "14"; 78 process.env.REDIS_URL = "redis://localhost:6379"; 79 80 const config = await loadConfig(); 81 82 expect(config.oauthPublicUrl).toBe("https://forum.example.com"); 83 expect(config.sessionSecret).toBe("my-super-secret-key-that-is-32-chars"); 84 expect(config.sessionTtlDays).toBe(14); 85 expect(config.redisUrl).toBe("redis://localhost:6379"); 86 }); 87 88 it("uses default values for optional OAuth config", async () => { 89 delete process.env.OAUTH_PUBLIC_URL; 90 delete process.env.SESSION_TTL_DAYS; 91 delete process.env.REDIS_URL; 92 93 const config = await loadConfig(); 94 95 expect(config.oauthPublicUrl).toBe("http://localhost:3000"); 96 expect(config.sessionTtlDays).toBe(7); 97 expect(config.redisUrl).toBeUndefined(); 98 }); 99 100 it("throws error when SESSION_SECRET is missing", async () => { 101 delete process.env.SESSION_SECRET; 102 103 await expect(loadConfig()).rejects.toThrow( 104 "SESSION_SECRET must be at least 32 characters" 105 ); 106 }); 107 108 it("throws error when SESSION_SECRET is too short", async () => { 109 process.env.SESSION_SECRET = "too-short"; 110 111 await expect(loadConfig()).rejects.toThrow( 112 "SESSION_SECRET must be at least 32 characters" 113 ); 114 }); 115 116 it("accepts SESSION_SECRET with exactly 32 characters", async () => { 117 process.env.SESSION_SECRET = "12345678901234567890123456789012"; // exactly 32 chars 118 119 const config = await loadConfig(); 120 121 expect(config.sessionSecret).toBe("12345678901234567890123456789012"); 122 }); 123 124 it("throws error when OAUTH_PUBLIC_URL is missing in production", async () => { 125 process.env.NODE_ENV = "production"; 126 delete process.env.OAUTH_PUBLIC_URL; 127 128 await expect(loadConfig()).rejects.toThrow( 129 "OAUTH_PUBLIC_URL is required in production" 130 ); 131 }); 132 133 it("allows missing OAUTH_PUBLIC_URL in development", async () => { 134 delete process.env.NODE_ENV; 135 delete process.env.OAUTH_PUBLIC_URL; 136 137 const config = await loadConfig(); 138 139 expect(config.oauthPublicUrl).toBe("http://localhost:3000"); 140 }); 141 142 it("warns about in-memory sessions in production", async () => { 143 const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); 144 process.env.NODE_ENV = "production"; 145 process.env.OAUTH_PUBLIC_URL = "https://example.com"; 146 delete process.env.REDIS_URL; 147 148 await loadConfig(); 149 150 expect(warnSpy).toHaveBeenCalledWith( 151 expect.stringContaining("in-memory session storage in production") 152 ); 153 154 warnSpy.mockRestore(); 155 }); 156 157 it("does not warn about in-memory sessions when REDIS_URL is set", async () => { 158 const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); 159 process.env.NODE_ENV = "production"; 160 process.env.OAUTH_PUBLIC_URL = "https://example.com"; 161 process.env.REDIS_URL = "redis://localhost:6379"; 162 163 await loadConfig(); 164 165 expect(warnSpy).not.toHaveBeenCalled(); 166 167 warnSpy.mockRestore(); 168 }); 169 170 it("does not warn about in-memory sessions in development", async () => { 171 const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); 172 delete process.env.NODE_ENV; 173 delete process.env.REDIS_URL; 174 175 await loadConfig(); 176 177 expect(warnSpy).not.toHaveBeenCalledWith( 178 expect.stringContaining("in-memory session storage") 179 ); 180 181 warnSpy.mockRestore(); 182 }); 183 }); 184 185 describe("Forum credentials", () => { 186 it("loads forum credentials from environment", async () => { 187 process.env.FORUM_HANDLE = "forum.example.com"; 188 process.env.FORUM_PASSWORD = "test-password"; 189 190 const config = await loadConfig(); 191 192 expect(config.forumHandle).toBe("forum.example.com"); 193 expect(config.forumPassword).toBe("test-password"); 194 }); 195 196 it("allows missing forum credentials (optional)", async () => { 197 delete process.env.FORUM_HANDLE; 198 delete process.env.FORUM_PASSWORD; 199 200 const config = await loadConfig(); 201 202 expect(config.forumHandle).toBeUndefined(); 203 expect(config.forumPassword).toBeUndefined(); 204 }); 205 }); 206 207 describe("Backfill configuration", () => { 208 it("uses default backfill values when env vars not set", async () => { 209 delete process.env.BACKFILL_RATE_LIMIT; 210 delete process.env.BACKFILL_CONCURRENCY; 211 delete process.env.BACKFILL_CURSOR_MAX_AGE_HOURS; 212 213 const config = await loadConfig(); 214 215 expect(config.backfillRateLimit).toBe(10); 216 expect(config.backfillConcurrency).toBe(10); 217 expect(config.backfillCursorMaxAgeHours).toBe(48); 218 }); 219 220 it("reads backfill values from env vars", async () => { 221 process.env.BACKFILL_RATE_LIMIT = "5"; 222 process.env.BACKFILL_CONCURRENCY = "20"; 223 process.env.BACKFILL_CURSOR_MAX_AGE_HOURS = "24"; 224 225 const config = await loadConfig(); 226 227 expect(config.backfillRateLimit).toBe(5); 228 expect(config.backfillConcurrency).toBe(20); 229 expect(config.backfillCursorMaxAgeHours).toBe(24); 230 }); 231 }); 232});