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 main 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});