config_test.mx raw
1 // Package config tests drive Load through a controlled environment. Every knob
2 // Load reads is named in testEnv with a non-default value; TestLoadDefaults
3 // clears the same set to assert the defaults. Helpers are exercised directly so
4 // the parse/trim/duration branches are covered without a whole Load.
5 //
6 // Environment mutations are captured in a map and restored by a deferred free
7 // function: a deferred closure cannot capture the map, and the harness has no
8 // t.Setenv.
9 package config
10
11 import (
12 "os"
13 "testing"
14 )
15
16 // testEnv is every ORLY_* variable Load reads, with a non-default value.
17 var testEnv = map[string]string{
18 "ORLY_APP_NAME": "SMESH",
19 "ORLY_DATA_DIR": "/srv/smesh",
20 "ORLY_LISTEN": "127.0.0.1",
21 "ORLY_PORT": "4444",
22 "ORLY_HEALTH_PORT": "8888",
23 "ORLY_LOG_LEVEL": "debug",
24 "ORLY_LOG_TO_STDOUT": "true",
25 "ORLY_LOG_BUFFER_SIZE": "777",
26 "ORLY_RELAY_URL": "wss://relay.example.com",
27 "ORLY_RELAY_ADDRESSES": "a,b,c",
28 "ORLY_RELAY_PEERS": "p1,p2",
29 "ORLY_SYNC_PUBKEY": "deadbeef",
30 "ORLY_CLIENT_TAG": "mytag",
31 "ORLY_AUTH_REQUIRED": "true",
32 "ORLY_AUTH_TO_WRITE": "true",
33 "ORLY_PRIVILEGED_OPEN": "true",
34 "ORLY_NIP70_ENFORCE": "false",
35 "ORLY_MARMOT_OPEN": "true",
36 "ORLY_NIP46_BYPASS_AUTH": "true",
37 "ORLY_ACL_MODE": "managed",
38 "ORLY_MUTE_BLACKLIST": "beef",
39 "ORLY_ADMINS": "adm1,adm2",
40 "ORLY_OWNERS": "own1",
41 "ORLY_FREE_WRITE_LIMIT": "5",
42 "ORLY_FREE_WRITE_WINDOW": "60",
43 "ORLY_MAX_CONN_PER_IP": "11",
44 "ORLY_INGEST_WORKERS": "3",
45 "ORLY_MEDIA_PROXY_WORKERS": "9",
46 "ORLY_BLOSSOM_WORKERS": "5",
47 "ORLY_MAX_GLOBAL_CONNECTIONS": "77",
48 "ORLY_MAX_SUBSCRIPTIONS": "88",
49 "ORLY_CONN_DELAY_MAX_MS": "123",
50 "ORLY_IP_WHITELIST": "10.0.0.0/8,192.168.0.1",
51 "ORLY_IP_BLACKLIST": "1.2.3.4",
52 "ORLY_HTTP_GUARD_BOT_BLOCK": "false",
53 "ORLY_RATE_LIMIT_ENABLED": "false",
54 "ORLY_RATE_LIMIT_TARGET_MB": "9",
55 "ORLY_RATE_LIMIT_WRITE_KP": "1.5",
56 "ORLY_RATE_LIMIT_WRITE_KI": "0.25",
57 "ORLY_RATE_LIMIT_WRITE_KD": "0.125",
58 "ORLY_RATE_LIMIT_READ_KP": "0.75",
59 "ORLY_RATE_LIMIT_READ_KI": "0.0625",
60 "ORLY_RATE_LIMIT_READ_KD": "0.03125",
61 "ORLY_RATE_LIMIT_MAX_WRITE_MS": "1500",
62 "ORLY_RATE_LIMIT_MAX_READ_MS": "600",
63 "ORLY_RATE_LIMIT_WRITE_TARGET": "0.875",
64 "ORLY_RATE_LIMIT_READ_TARGET": "0.625",
65 "ORLY_RATE_LIMIT_EMERGENCY_THRESHOLD": "1.5",
66 "ORLY_RATE_LIMIT_RECOVERY_THRESHOLD": "0.25",
67 "ORLY_RATE_LIMIT_EMERGENCY_MAX_MS": "6000",
68 "ORLY_QUERY_RESULT_LIMIT": "512",
69 "ORLY_FOLLOW_LIST_FREQUENCY": "2h",
70 "ORLY_FOLLOWS_THROTTLE": "true",
71 "ORLY_FOLLOWS_THROTTLE_INCREMENT": "30ms",
72 "ORLY_FOLLOWS_THROTTLE_MAX": "90s",
73 "ORLY_SOCIAL_THROTTLE_D2_INCREMENT": "55ms",
74 "ORLY_SOCIAL_THROTTLE_D2_MAX": "31s",
75 "ORLY_SOCIAL_THROTTLE_D3_INCREMENT": "210ms",
76 "ORLY_SOCIAL_THROTTLE_D3_MAX": "61s",
77 "ORLY_SOCIAL_THROTTLE_OUTSIDER_INCREMENT": "510ms",
78 "ORLY_SOCIAL_THROTTLE_OUTSIDER_MAX": "121s",
79 "ORLY_SOCIAL_WOT_DEPTH": "4",
80 "ORLY_SOCIAL_WOT_REFRESH": "2h",
81 "ORLY_GRAPEVINE_ENABLED": "true",
82 "ORLY_GRAPEVINE_MAX_DEPTH": "7",
83 "ORLY_GRAPEVINE_MAX_CYCLES": "21",
84 "ORLY_GRAPEVINE_ATTENUATION": "0.5",
85 "ORLY_GRAPEVINE_RIGOR": "0.75",
86 "ORLY_GRAPEVINE_FOLLOW_CONFIDENCE": "0.125",
87 "ORLY_GRAPEVINE_OBSERVERS": "obs1,obs2",
88 "ORLY_GRAPEVINE_REFRESH": "7h",
89 "ORLY_GRAPEVINE_AUTO_WHITELIST": "true",
90 "ORLY_GRAPEVINE_WHITELIST_THRESHOLD": "0.375",
91 "ORLY_GRAPEVINE_WHITELIST_REFRESH": "8h",
92 "ORLY_GRAPH_QUERIES_ENABLED": "false",
93 "ORLY_GRAPH_MAX_DEPTH": "17",
94 "ORLY_GRAPH_MAX_RESULTS": "20000",
95 "ORLY_GRAPH_RATE_LIMIT_RPM": "61",
96 "ORLY_PROXY_ENABLED": "false",
97 "ORLY_PROXY_MAX_RELAYS": "17",
98 "ORLY_PROXY_TIMEOUT_SEC": "16",
99 "ORLY_ARCHIVE_ENABLED": "true",
100 "ORLY_ARCHIVE_RELAYS": "wss://arch.example/",
101 "ORLY_ARCHIVE_TIMEOUT_SEC": "31",
102 "ORLY_ARCHIVE_CACHE_TTL_HRS": "25",
103 "ORLY_BLOSSOM_ENABLED": "false",
104 "ORLY_BLOSSOM_DIR": "/srv/blobs",
105 "ORLY_BLOSSOM_UPSTREAM": "https://up.example",
106 "ORLY_BLOSSOM_SERVICE_LEVELS": "s1:1",
107 "ORLY_BLOSSOM_RATE_LIMIT": "true",
108 "ORLY_BLOSSOM_DAILY_LIMIT_MB": "11",
109 "ORLY_BLOSSOM_BURST_LIMIT_MB": "51",
110 "ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG": "true",
111 "ORLY_BOOTSTRAP_RELAYS": "wss://b1/",
112 "ORLY_SPIDER_MODE": "follows",
113 "ORLY_DIRECTORY_SPIDER": "true",
114 "ORLY_DIRECTORY_SPIDER_INTERVAL": "25h",
115 "ORLY_DIRECTORY_SPIDER_HOPS": "4",
116 "ORLY_CRAWLER_ENABLED": "true",
117 "ORLY_CRAWLER_DISCOVERY_INTERVAL": "5h",
118 "ORLY_CRAWLER_SYNC_INTERVAL": "31m",
119 "ORLY_CRAWLER_MAX_HOPS": "6",
120 "ORLY_CRAWLER_CONCURRENCY": "4",
121 "ORLY_NEGENTROPY_ENABLED": "true",
122 "ORLY_NEGENTROPY_FULL_SYNC_PUBKEYS": "npub1",
123 "ORLY_TLS_DOMAINS": "d1,d2",
124 "ORLY_CERTS": "c1",
125 "ORLY_TOR_ENABLED": "false",
126 "ORLY_TOR_PORT": "3337",
127 "ORLY_TOR_DATA_DIR": "/srv/tor",
128 "ORLY_TOR_BINARY": "/usr/bin/tor",
129 "ORLY_TOR_SOCKS": "9050",
130 "ORLY_NRC_ENABLED": "false",
131 "ORLY_NRC_RENDEZVOUS_URL": "wss://nrc/",
132 "ORLY_NRC_AUTHORIZED_KEYS": "k1",
133 "ORLY_NRC_SESSION_TIMEOUT": "31m",
134 "ORLY_WG_ENABLED": "true",
135 "ORLY_WG_PORT": "51821",
136 "ORLY_WG_ENDPOINT": "vpn.example",
137 "ORLY_WG_NETWORK": "10.74.0.0/16",
138 "ORLY_BUNKER_ENABLED": "true",
139 "ORLY_BUNKER_PORT": "3338",
140 "ORLY_NWC_URI": "nostr+walletconnect://x",
141 "ORLY_SUBSCRIPTION_ENABLED": "true",
142 "ORLY_MONTHLY_PRICE_SATS": "7000",
143 "ORLY_NIP43_ENABLED": "true",
144 "ORLY_NIP43_PUBLISH_EVENTS": "false",
145 "ORLY_NIP43_PUBLISH_MEMBER_LIST": "false",
146 "ORLY_NIP43_INVITE_EXPIRY": "25h",
147 "ORLY_POLICY_ENABLED": "true",
148 "ORLY_POLICY_PATH": "/srv/policy.json",
149 "ORLY_MAX_STORAGE_BYTES": "123456",
150 "ORLY_GC_ENABLED": "true",
151 "ORLY_GC_INTERVAL_SEC": "61",
152 "ORLY_GC_BATCH_SIZE": "1001",
153 "ORLY_BRIDGE_ENABLED": "true",
154 "ORLY_BRIDGE_DOMAIN": "mail.example",
155 "ORLY_BRIDGE_NSEC": "nsec1",
156 "ORLY_BRIDGE_RELAY_URL": "wss://br/",
157 "ORLY_BRIDGE_PUBLIC_RELAY_URL": "wss://bpr/",
158 "ORLY_BRIDGE_SMTP_PORT": "2526",
159 "ORLY_BRIDGE_SMTP_HOST": "127.0.0.1",
160 "ORLY_BRIDGE_DATA_DIR": "/srv/bridge",
161 "ORLY_BRIDGE_DKIM_KEY": "/srv/dkim",
162 "ORLY_BRIDGE_DKIM_SELECTOR": "sel",
163 "ORLY_BRIDGE_NWC_URI": "nwc://x",
164 "ORLY_BRIDGE_MONTHLY_PRICE_SATS": "2200",
165 "ORLY_BRIDGE_COMPOSE_URL": "https://compose/",
166 "ORLY_BRIDGE_SMTP_RELAY_HOST": "smtp.example",
167 "ORLY_BRIDGE_SMTP_RELAY_PORT": "588",
168 "ORLY_BRIDGE_SMTP_MX_PORT": "26",
169 "ORLY_BRIDGE_SMTP_RELAY_USERNAME": "user",
170 "ORLY_BRIDGE_SMTP_RELAY_PASSWORD": "pass",
171 "ORLY_BRIDGE_ALIAS_PRICE_SATS": "4300",
172 "ORLY_BRIDGE_PROFILE": "/srv/profile",
173 "ORLY_CLUSTER_ADMINS": "ca1,ca2",
174 "ORLY_RELAY_GROUP_ADMINS": "rga1",
175 "ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS": "false",
176 "ORLY_SMESH_ENABLED": "false",
177 "ORLY_SMESH_PORT": "8091",
178 "ORLY_SMESH2_ENABLED": "false",
179 "ORLY_SMESH2_PORT": "8092",
180 "ORLY_SMESH3_ENABLED": "false",
181 "ORLY_SMESH3_PORT": "8093",
182 "ORLY_SMESH3_DIR": "/srv/smesh3",
183 "ORLY_DEPLOY_PUBKEY": "deploy",
184 "ORLY_STATIC_DIR": "web/custom",
185 "ORLY_WEB_DISABLE": "true",
186 "ORLY_WEB_DEV_PROXY_URL": "http://localhost:5173",
187 "ORLY_BRANDING_DIR": "/srv/brand",
188 "ORLY_BRANDING_ENABLED": "false",
189 "ORLY_THEME": "dark",
190 "ORLY_CORS_ENABLED": "true",
191 "ORLY_CORS_ORIGINS": "https://a.example,https://b.example",
192 "ORLY_SPROCKET_ENABLED": "true",
193 "ORLY_ENABLE_SHUTDOWN": "true",
194 }
195
196 func guardSet(g map[string]string, k, v string) {
197 g[k] = os.Getenv(k)
198 os.Setenv(k, v)
199 }
200
201 func guardUnset(g map[string]string, k string) {
202 g[k] = os.Getenv(k)
203 os.Unsetenv(k)
204 }
205
206 func guardRestore(g map[string]string) {
207 for k, v := range g {
208 if v == "" {
209 os.Unsetenv(k)
210 } else {
211 os.Setenv(k, v)
212 }
213 }
214 }
215
216 func clearTestEnv(g map[string]string) {
217 for k := range testEnv {
218 guardUnset(g, k)
219 }
220 }
221
222 func setTestEnv(g map[string]string) {
223 for k, v := range testEnv {
224 guardSet(g, k, v)
225 }
226 }
227
228 func chkI32(t *testing.T, name string, got, want int32) {
229 if got != want {
230 t.Fatalf("%s = %d, want %d", name, got, want)
231 }
232 }
233
234 func chkI64(t *testing.T, name string, got, want int64) {
235 if got != want {
236 t.Fatalf("%s = %d, want %d", name, got, want)
237 }
238 }
239
240 func chkF(t *testing.T, name string, got, want float64) {
241 d := got - want
242 if d < 0 {
243 d = -d
244 }
245 if d > 0.000001 {
246 t.Fatalf("%s = %f, want %f", name, got, want)
247 }
248 }
249
250 func chkS(t *testing.T, name, got, want string) {
251 if got != want {
252 t.Fatalf("%s = %s, want %s", name, got, want)
253 }
254 }
255
256 func chkB(t *testing.T, name string, got, want bool) {
257 if got != want {
258 if want {
259 t.Fatalf("%s = false, want true", name)
260 }
261 t.Fatalf("%s = true, want false", name)
262 }
263 }
264
265 func chkStrs(t *testing.T, name string, got []string, want []string) {
266 if len(got) != len(want) {
267 t.Fatalf("%s len = %d, want %d", name, int32(len(got)), int32(len(want)))
268 }
269 for i := 0; i < len(got); i++ {
270 if got[i] != want[i] {
271 t.Fatalf("%s[%d] = %s, want %s", name, i, got[i], want[i])
272 }
273 }
274 }
275
276 // TestLoadDefaults clears every knob and asserts what Load produces from an
277 // empty environment, including the two derived defaults (ArchiveRelays and
278 // BlossomDir).
279 func TestLoadDefaults(t *testing.T) {
280 g := map[string]string{}
281 defer guardRestore(g)
282 clearTestEnv(g)
283 guardUnset(g, "HOME")
284 dotenv = nil
285
286 c := Load()
287 if c == nil {
288 t.Fatal("Load returned nil")
289 }
290 chkS(t, "AppName", c.AppName, "ORLY")
291 chkS(t, "DataDir", c.DataDir, "~/.local/share/ORLY")
292 chkS(t, "Listen", c.Listen, "0.0.0.0")
293 chkI32(t, "Port", c.Port, 3334)
294 chkI32(t, "HealthPort", c.HealthPort, 0)
295 chkS(t, "LogLevel", c.LogLevel, "info")
296 chkB(t, "LogToStdout", c.LogToStdout, false)
297 chkI32(t, "LogBufferSize", c.LogBufferSize, 10000)
298 chkS(t, "RelayURL", c.RelayURL, "")
299 chkI32(t, "RelayAddresses len", int32(len(c.RelayAddresses)), 0)
300 chkI32(t, "RelayPeers len", int32(len(c.RelayPeers)), 0)
301 chkS(t, "SyncPubkey", c.SyncPubkey, "")
302 chkS(t, "ClientTag", c.ClientTag, "git.smesh.lol/smesh")
303 chkB(t, "AuthRequired", c.AuthRequired, false)
304 chkB(t, "AuthToWrite", c.AuthToWrite, false)
305 chkB(t, "PrivilegedOpen", c.PrivilegedOpen, false)
306 chkB(t, "NIP70Enforce", c.NIP70Enforce, true)
307 chkB(t, "MarmotOpen", c.MarmotOpen, false)
308 chkB(t, "NIP46BypassAuth", c.NIP46BypassAuth, false)
309 chkS(t, "ACLMode", c.ACLMode, "none")
310 chkS(t, "MuteBlacklist", c.MuteBlacklist, "")
311 chkI32(t, "Admins len", int32(len(c.Admins)), 0)
312 chkI32(t, "Owners len", int32(len(c.Owners)), 0)
313 chkI32(t, "FreeWriteLimit", c.FreeWriteLimit, 25)
314 chkI32(t, "FreeWriteWindow", c.FreeWriteWindow, 300)
315 chkI32(t, "MaxConnPerIP", c.MaxConnPerIP, 100)
316 chkI32(t, "IngestWorkers", c.IngestWorkers, 0)
317 chkI32(t, "MediaProxyWorkers", c.MediaProxyWorkers, 8)
318 chkI32(t, "BlossomWorkers", c.BlossomWorkers, 4)
319 chkI32(t, "MaxGlobalConns", c.MaxGlobalConns, 500)
320 chkI32(t, "MaxSubscriptions", c.MaxSubscriptions, 10000)
321 chkI32(t, "ConnDelayMaxMs", c.ConnDelayMaxMs, 2000)
322 chkI32(t, "IPWhitelist len", int32(len(c.IPWhitelist)), 0)
323 chkI32(t, "IPBlacklist len", int32(len(c.IPBlacklist)), 0)
324 chkB(t, "HTTPGuardBotBlock", c.HTTPGuardBotBlock, true)
325 chkB(t, "RateLimitEnabled", c.RateLimitEnabled, true)
326 chkI32(t, "RateLimitTargetMB", c.RateLimitTargetMB, 0)
327 chkF(t, "RateLimitWriteKp", c.RateLimitWriteKp, 0.5)
328 chkF(t, "RateLimitWriteKi", c.RateLimitWriteKi, 0.1)
329 chkF(t, "RateLimitWriteKd", c.RateLimitWriteKd, 0.05)
330 chkF(t, "RateLimitReadKp", c.RateLimitReadKp, 0.3)
331 chkF(t, "RateLimitReadKi", c.RateLimitReadKi, 0.05)
332 chkF(t, "RateLimitReadKd", c.RateLimitReadKd, 0.02)
333 chkI32(t, "RateLimitMaxWriteMs", c.RateLimitMaxWriteMs, 1000)
334 chkI32(t, "RateLimitMaxReadMs", c.RateLimitMaxReadMs, 500)
335 chkF(t, "RateLimitWriteTarget", c.RateLimitWriteTarget, 0.85)
336 chkF(t, "RateLimitReadTarget", c.RateLimitReadTarget, 0.90)
337 chkF(t, "EmergencyThreshold", c.EmergencyThreshold, 1.167)
338 chkF(t, "RecoveryThreshold", c.RecoveryThreshold, 0.833)
339 chkI32(t, "EmergencyMaxMs", c.EmergencyMaxMs, 5000)
340 chkI32(t, "QueryResultLimit", c.QueryResultLimit, 256)
341 chkI32(t, "FollowListFreqSec", c.FollowListFreqSec, 3600)
342 chkB(t, "FollowsThrottle", c.FollowsThrottle, false)
343 chkI32(t, "FollowsThrottleIncrMs", c.FollowsThrottleIncrMs, 25)
344 chkI32(t, "FollowsThrottleMaxMs", c.FollowsThrottleMaxMs, 60000)
345 chkI32(t, "SocialThrottleD2IncrMs", c.SocialThrottleD2IncrMs, 50)
346 chkI32(t, "SocialThrottleD2MaxMs", c.SocialThrottleD2MaxMs, 30000)
347 chkI32(t, "SocialThrottleD3IncrMs", c.SocialThrottleD3IncrMs, 200)
348 chkI32(t, "SocialThrottleD3MaxMs", c.SocialThrottleD3MaxMs, 60000)
349 chkI32(t, "SocialThrottleOutsiderIncrMs", c.SocialThrottleOutsiderIncrMs, 500)
350 chkI32(t, "SocialThrottleOutsiderMaxMs", c.SocialThrottleOutsiderMaxMs, 120000)
351 chkI32(t, "SocialWoTMaxDepth", c.SocialWoTMaxDepth, 3)
352 chkI32(t, "SocialWoTRefreshSec", c.SocialWoTRefreshSec, 3600)
353 chkB(t, "GrapeVineEnabled", c.GrapeVineEnabled, false)
354 chkI32(t, "GrapeVineMaxDepth", c.GrapeVineMaxDepth, 6)
355 chkI32(t, "GrapeVineMaxCycles", c.GrapeVineMaxCycles, 20)
356 chkF(t, "GrapeVineAttenuation", c.GrapeVineAttenuation, 0.8)
357 chkF(t, "GrapeVineRigor", c.GrapeVineRigor, 0.25)
358 chkF(t, "GrapeVineFollowConf", c.GrapeVineFollowConf, 0.05)
359 chkI32(t, "GrapeVineObservers len", int32(len(c.GrapeVineObservers)), 0)
360 chkI32(t, "GrapeVineRefreshSec", c.GrapeVineRefreshSec, 21600)
361 chkB(t, "GrapeVineAutoWhitelist", c.GrapeVineAutoWhitelist, false)
362 chkF(t, "GrapeVineWhitelistThresh", c.GrapeVineWhitelistThresh, 0.5)
363 chkI32(t, "GrapeVineWhitelistRefSec", c.GrapeVineWhitelistRefSec, 21600)
364 chkB(t, "GraphQueriesEnabled", c.GraphQueriesEnabled, true)
365 chkI32(t, "GraphMaxDepth", c.GraphMaxDepth, 16)
366 chkI32(t, "GraphMaxResults", c.GraphMaxResults, 10000)
367 chkI32(t, "GraphRateLimitRPM", c.GraphRateLimitRPM, 60)
368 chkB(t, "ProxyEnabled", c.ProxyEnabled, true)
369 chkI32(t, "ProxyMaxRelays", c.ProxyMaxRelays, 16)
370 chkI32(t, "ProxyTimeoutSec", c.ProxyTimeoutSec, 15)
371 chkB(t, "ArchiveEnabled", c.ArchiveEnabled, false)
372 chkStrs(t, "ArchiveRelays", c.ArchiveRelays, []string{"wss://archive.orly.dev/"})
373 chkI32(t, "ArchiveTimeoutSec", c.ArchiveTimeoutSec, 30)
374 chkI32(t, "ArchiveCacheTTLHrs", c.ArchiveCacheTTLHrs, 24)
375 chkB(t, "BlossomEnabled", c.BlossomEnabled, true)
376 chkS(t, "BlossomDir", c.BlossomDir, "~/.local/share/ORLY/blossom")
377 chkS(t, "BlossomUpstream", c.BlossomUpstream, "")
378 chkS(t, "BlossomServiceLevels", c.BlossomServiceLevels, "")
379 chkB(t, "BlossomRateLimit", c.BlossomRateLimit, false)
380 chkI64(t, "BlossomDailyLimitMB", c.BlossomDailyLimitMB, 10)
381 chkI64(t, "BlossomBurstLimitMB", c.BlossomBurstLimitMB, 50)
382 chkB(t, "BlossomDeleteRequireServerTag", c.BlossomDeleteRequireServerTag, false)
383 chkI32(t, "BootstrapRelays len", int32(len(c.BootstrapRelays)), 0)
384 chkS(t, "SpiderMode", c.SpiderMode, "none")
385 chkB(t, "DirectorySpider", c.DirectorySpider, false)
386 chkI32(t, "DirectorySpiderIntSec", c.DirectorySpiderIntSec, 86400)
387 chkI32(t, "DirectorySpiderMaxHops", c.DirectorySpiderMaxHops, 3)
388 chkB(t, "CrawlerEnabled", c.CrawlerEnabled, false)
389 chkI32(t, "CrawlerDiscoveryIntSec", c.CrawlerDiscoveryIntSec, 14400)
390 chkI32(t, "CrawlerSyncIntSec", c.CrawlerSyncIntSec, 1800)
391 chkI32(t, "CrawlerMaxHops", c.CrawlerMaxHops, 5)
392 chkI32(t, "CrawlerConcurrency", c.CrawlerConcurrency, 3)
393 chkB(t, "NegentropyEnabled", c.NegentropyEnabled, false)
394 chkS(t, "NegentropyFullSyncPubs", c.NegentropyFullSyncPubs, "")
395 chkI32(t, "TLSDomains len", int32(len(c.TLSDomains)), 0)
396 chkI32(t, "Certs len", int32(len(c.Certs)), 0)
397 chkB(t, "TorEnabled", c.TorEnabled, true)
398 chkI32(t, "TorPort", c.TorPort, 3336)
399 chkS(t, "TorDataDir", c.TorDataDir, "")
400 chkS(t, "TorBinary", c.TorBinary, "tor")
401 chkI32(t, "TorSOCKS", c.TorSOCKS, 0)
402 chkB(t, "NRCEnabled", c.NRCEnabled, true)
403 chkS(t, "NRCRendezvousURL", c.NRCRendezvousURL, "")
404 chkS(t, "NRCAuthorizedKeys", c.NRCAuthorizedKeys, "")
405 chkI32(t, "NRCSessionTimeSec", c.NRCSessionTimeSec, 1800)
406 chkB(t, "WGEnabled", c.WGEnabled, false)
407 chkI32(t, "WGPort", c.WGPort, 51820)
408 chkS(t, "WGEndpoint", c.WGEndpoint, "")
409 chkS(t, "WGNetwork", c.WGNetwork, "10.73.0.0/16")
410 chkB(t, "BunkerEnabled", c.BunkerEnabled, false)
411 chkI32(t, "BunkerPort", c.BunkerPort, 3335)
412 chkS(t, "NWCUri", c.NWCUri, "")
413 chkB(t, "SubscriptionEnabled", c.SubscriptionEnabled, false)
414 chkI64(t, "MonthlyPriceSats", c.MonthlyPriceSats, 6000)
415 chkB(t, "NIP43Enabled", c.NIP43Enabled, false)
416 chkB(t, "NIP43PublishEvents", c.NIP43PublishEvents, true)
417 chkB(t, "NIP43PublishMembers", c.NIP43PublishMembers, true)
418 chkI32(t, "NIP43InviteExpSec", c.NIP43InviteExpSec, 86400)
419 chkB(t, "PolicyEnabled", c.PolicyEnabled, false)
420 chkS(t, "PolicyPath", c.PolicyPath, "")
421 chkI64(t, "MaxStorageBytes", c.MaxStorageBytes, 0)
422 chkB(t, "GCEnabled", c.GCEnabled, false)
423 chkI32(t, "GCIntervalSec", c.GCIntervalSec, 60)
424 chkI32(t, "GCBatchSize", c.GCBatchSize, 1000)
425 chkB(t, "BridgeEnabled", c.BridgeEnabled, false)
426 chkS(t, "BridgeDomain", c.BridgeDomain, "")
427 chkS(t, "BridgeNSEC", c.BridgeNSEC, "")
428 chkS(t, "BridgeRelayURL", c.BridgeRelayURL, "")
429 chkS(t, "BridgePublicRelayURL", c.BridgePublicRelayURL, "")
430 chkI32(t, "BridgeSMTPPort", c.BridgeSMTPPort, 2525)
431 chkS(t, "BridgeSMTPHost", c.BridgeSMTPHost, "0.0.0.0")
432 chkS(t, "BridgeDataDir", c.BridgeDataDir, "")
433 chkS(t, "BridgeDKIMKeyPath", c.BridgeDKIMKeyPath, "")
434 chkS(t, "BridgeDKIMSelector", c.BridgeDKIMSelector, "marmot")
435 chkS(t, "BridgeNWCURI", c.BridgeNWCURI, "")
436 chkI64(t, "BridgeMonthlyPriceSats", c.BridgeMonthlyPriceSats, 2100)
437 chkS(t, "BridgeComposeURL", c.BridgeComposeURL, "")
438 chkS(t, "BridgeSMTPRelayHost", c.BridgeSMTPRelayHost, "")
439 chkI32(t, "BridgeSMTPRelayPort", c.BridgeSMTPRelayPort, 587)
440 chkI32(t, "BridgeSMTPMXPort", c.BridgeSMTPMXPort, 0)
441 chkS(t, "BridgeSMTPRelayUser", c.BridgeSMTPRelayUser, "")
442 chkS(t, "BridgeSMTPRelayPass", c.BridgeSMTPRelayPass, "")
443 chkI64(t, "BridgeAliasPriceSats", c.BridgeAliasPriceSats, 4200)
444 chkS(t, "BridgeProfile", c.BridgeProfile, "")
445 chkI32(t, "ClusterAdmins len", int32(len(c.ClusterAdmins)), 0)
446 chkI32(t, "RelayGroupAdmins len", int32(len(c.RelayGroupAdmins)), 0)
447 chkB(t, "ClusterPropPrivileged", c.ClusterPropPrivileged, true)
448 chkB(t, "SmeshEnabled", c.SmeshEnabled, true)
449 chkI32(t, "SmeshPort", c.SmeshPort, 8088)
450 chkB(t, "Smesh2Enabled", c.Smesh2Enabled, true)
451 chkI32(t, "Smesh2Port", c.Smesh2Port, 8089)
452 chkB(t, "Smesh3Enabled", c.Smesh3Enabled, true)
453 chkI32(t, "Smesh3Port", c.Smesh3Port, 8090)
454 chkS(t, "Smesh3Dir", c.Smesh3Dir, "")
455 chkS(t, "DeployPubkey", c.DeployPubkey, "")
456 chkS(t, "StaticDir", c.StaticDir, "web/static")
457 chkB(t, "WebDisable", c.WebDisable, false)
458 chkS(t, "WebDevProxyURL", c.WebDevProxyURL, "")
459 chkS(t, "BrandingDir", c.BrandingDir, "")
460 chkB(t, "BrandingEnabled", c.BrandingEnabled, true)
461 chkS(t, "Theme", c.Theme, "auto")
462 chkB(t, "CORSEnabled", c.CORSEnabled, false)
463 chkI32(t, "CORSOrigins len", int32(len(c.CORSOrigins)), 0)
464 chkB(t, "SprocketEnabled", c.SprocketEnabled, false)
465 chkB(t, "EnableShutdown", c.EnableShutdown, false)
466 chkS(t, "Addr", c.Addr(), "0.0.0.0:3334")
467 }
468
469 // TestLoadEnvOverride sets every knob to a non-default value and asserts each
470 // parsed field, so a change to any default or parser fails here.
471 func TestLoadEnvOverride(t *testing.T) {
472 g := map[string]string{}
473 defer guardRestore(g)
474 guardUnset(g, "HOME")
475 dotenv = nil
476 setTestEnv(g)
477
478 c := Load()
479 chkS(t, "AppName", c.AppName, "SMESH")
480 chkS(t, "DataDir", c.DataDir, "/srv/smesh")
481 chkS(t, "Listen", c.Listen, "127.0.0.1")
482 chkI32(t, "Port", c.Port, 4444)
483 chkI32(t, "HealthPort", c.HealthPort, 8888)
484 chkS(t, "LogLevel", c.LogLevel, "debug")
485 chkB(t, "LogToStdout", c.LogToStdout, true)
486 chkI32(t, "LogBufferSize", c.LogBufferSize, 777)
487 chkS(t, "RelayURL", c.RelayURL, "wss://relay.example.com")
488 chkStrs(t, "RelayAddresses", c.RelayAddresses, []string{"a", "b", "c"})
489 chkStrs(t, "RelayPeers", c.RelayPeers, []string{"p1", "p2"})
490 chkS(t, "SyncPubkey", c.SyncPubkey, "deadbeef")
491 chkS(t, "ClientTag", c.ClientTag, "mytag")
492 chkB(t, "AuthRequired", c.AuthRequired, true)
493 chkB(t, "AuthToWrite", c.AuthToWrite, true)
494 chkB(t, "PrivilegedOpen", c.PrivilegedOpen, true)
495 chkB(t, "NIP70Enforce", c.NIP70Enforce, false)
496 chkB(t, "MarmotOpen", c.MarmotOpen, true)
497 chkB(t, "NIP46BypassAuth", c.NIP46BypassAuth, true)
498 chkS(t, "ACLMode", c.ACLMode, "managed")
499 chkS(t, "MuteBlacklist", c.MuteBlacklist, "beef")
500 chkStrs(t, "Admins", c.Admins, []string{"adm1", "adm2"})
501 chkStrs(t, "Owners", c.Owners, []string{"own1"})
502 chkI32(t, "FreeWriteLimit", c.FreeWriteLimit, 5)
503 chkI32(t, "FreeWriteWindow", c.FreeWriteWindow, 60)
504 chkI32(t, "MaxConnPerIP", c.MaxConnPerIP, 11)
505 chkI32(t, "IngestWorkers", c.IngestWorkers, 3)
506 chkI32(t, "MediaProxyWorkers", c.MediaProxyWorkers, 9)
507 chkI32(t, "BlossomWorkers", c.BlossomWorkers, 5)
508 chkI32(t, "MaxGlobalConns", c.MaxGlobalConns, 77)
509 chkI32(t, "MaxSubscriptions", c.MaxSubscriptions, 88)
510 chkI32(t, "ConnDelayMaxMs", c.ConnDelayMaxMs, 123)
511 chkStrs(t, "IPWhitelist", c.IPWhitelist, []string{"10.0.0.0/8", "192.168.0.1"})
512 chkStrs(t, "IPBlacklist", c.IPBlacklist, []string{"1.2.3.4"})
513 chkB(t, "HTTPGuardBotBlock", c.HTTPGuardBotBlock, false)
514 chkB(t, "RateLimitEnabled", c.RateLimitEnabled, false)
515 chkI32(t, "RateLimitTargetMB", c.RateLimitTargetMB, 9)
516 chkF(t, "RateLimitWriteKp", c.RateLimitWriteKp, 1.5)
517 chkF(t, "RateLimitWriteKi", c.RateLimitWriteKi, 0.25)
518 chkF(t, "RateLimitWriteKd", c.RateLimitWriteKd, 0.125)
519 chkF(t, "RateLimitReadKp", c.RateLimitReadKp, 0.75)
520 chkF(t, "RateLimitReadKi", c.RateLimitReadKi, 0.0625)
521 chkF(t, "RateLimitReadKd", c.RateLimitReadKd, 0.03125)
522 chkI32(t, "RateLimitMaxWriteMs", c.RateLimitMaxWriteMs, 1500)
523 chkI32(t, "RateLimitMaxReadMs", c.RateLimitMaxReadMs, 600)
524 chkF(t, "RateLimitWriteTarget", c.RateLimitWriteTarget, 0.875)
525 chkF(t, "RateLimitReadTarget", c.RateLimitReadTarget, 0.625)
526 chkF(t, "EmergencyThreshold", c.EmergencyThreshold, 1.5)
527 chkF(t, "RecoveryThreshold", c.RecoveryThreshold, 0.25)
528 chkI32(t, "EmergencyMaxMs", c.EmergencyMaxMs, 6000)
529 chkI32(t, "QueryResultLimit", c.QueryResultLimit, 512)
530 chkI32(t, "FollowListFreqSec", c.FollowListFreqSec, 7200)
531 chkB(t, "FollowsThrottle", c.FollowsThrottle, true)
532 chkI32(t, "FollowsThrottleIncrMs", c.FollowsThrottleIncrMs, 30)
533 chkI32(t, "FollowsThrottleMaxMs", c.FollowsThrottleMaxMs, 90000)
534 chkI32(t, "SocialThrottleD2IncrMs", c.SocialThrottleD2IncrMs, 55)
535 chkI32(t, "SocialThrottleD2MaxMs", c.SocialThrottleD2MaxMs, 31000)
536 chkI32(t, "SocialThrottleD3IncrMs", c.SocialThrottleD3IncrMs, 210)
537 chkI32(t, "SocialThrottleD3MaxMs", c.SocialThrottleD3MaxMs, 61000)
538 chkI32(t, "SocialThrottleOutsiderIncrMs", c.SocialThrottleOutsiderIncrMs, 510)
539 chkI32(t, "SocialThrottleOutsiderMaxMs", c.SocialThrottleOutsiderMaxMs, 121000)
540 chkI32(t, "SocialWoTMaxDepth", c.SocialWoTMaxDepth, 4)
541 chkI32(t, "SocialWoTRefreshSec", c.SocialWoTRefreshSec, 7200)
542 chkB(t, "GrapeVineEnabled", c.GrapeVineEnabled, true)
543 chkI32(t, "GrapeVineMaxDepth", c.GrapeVineMaxDepth, 7)
544 chkI32(t, "GrapeVineMaxCycles", c.GrapeVineMaxCycles, 21)
545 chkF(t, "GrapeVineAttenuation", c.GrapeVineAttenuation, 0.5)
546 chkF(t, "GrapeVineRigor", c.GrapeVineRigor, 0.75)
547 chkF(t, "GrapeVineFollowConf", c.GrapeVineFollowConf, 0.125)
548 chkStrs(t, "GrapeVineObservers", c.GrapeVineObservers, []string{"obs1", "obs2"})
549 chkI32(t, "GrapeVineRefreshSec", c.GrapeVineRefreshSec, 25200)
550 chkB(t, "GrapeVineAutoWhitelist", c.GrapeVineAutoWhitelist, true)
551 chkF(t, "GrapeVineWhitelistThresh", c.GrapeVineWhitelistThresh, 0.375)
552 chkI32(t, "GrapeVineWhitelistRefSec", c.GrapeVineWhitelistRefSec, 28800)
553 chkB(t, "GraphQueriesEnabled", c.GraphQueriesEnabled, false)
554 chkI32(t, "GraphMaxDepth", c.GraphMaxDepth, 17)
555 chkI32(t, "GraphMaxResults", c.GraphMaxResults, 20000)
556 chkI32(t, "GraphRateLimitRPM", c.GraphRateLimitRPM, 61)
557 chkB(t, "ProxyEnabled", c.ProxyEnabled, false)
558 chkI32(t, "ProxyMaxRelays", c.ProxyMaxRelays, 17)
559 chkI32(t, "ProxyTimeoutSec", c.ProxyTimeoutSec, 16)
560 chkB(t, "ArchiveEnabled", c.ArchiveEnabled, true)
561 chkStrs(t, "ArchiveRelays", c.ArchiveRelays, []string{"wss://arch.example/"})
562 chkI32(t, "ArchiveTimeoutSec", c.ArchiveTimeoutSec, 31)
563 chkI32(t, "ArchiveCacheTTLHrs", c.ArchiveCacheTTLHrs, 25)
564 chkB(t, "BlossomEnabled", c.BlossomEnabled, false)
565 chkS(t, "BlossomDir", c.BlossomDir, "/srv/blobs")
566 chkS(t, "BlossomUpstream", c.BlossomUpstream, "https://up.example")
567 chkS(t, "BlossomServiceLevels", c.BlossomServiceLevels, "s1:1")
568 chkB(t, "BlossomRateLimit", c.BlossomRateLimit, true)
569 chkI64(t, "BlossomDailyLimitMB", c.BlossomDailyLimitMB, 11)
570 chkI64(t, "BlossomBurstLimitMB", c.BlossomBurstLimitMB, 51)
571 chkB(t, "BlossomDeleteRequireServerTag", c.BlossomDeleteRequireServerTag, true)
572 chkStrs(t, "BootstrapRelays", c.BootstrapRelays, []string{"wss://b1/"})
573 chkS(t, "SpiderMode", c.SpiderMode, "follows")
574 chkB(t, "DirectorySpider", c.DirectorySpider, true)
575 chkI32(t, "DirectorySpiderIntSec", c.DirectorySpiderIntSec, 90000)
576 chkI32(t, "DirectorySpiderMaxHops", c.DirectorySpiderMaxHops, 4)
577 chkB(t, "CrawlerEnabled", c.CrawlerEnabled, true)
578 chkI32(t, "CrawlerDiscoveryIntSec", c.CrawlerDiscoveryIntSec, 18000)
579 chkI32(t, "CrawlerSyncIntSec", c.CrawlerSyncIntSec, 1860)
580 chkI32(t, "CrawlerMaxHops", c.CrawlerMaxHops, 6)
581 chkI32(t, "CrawlerConcurrency", c.CrawlerConcurrency, 4)
582 chkB(t, "NegentropyEnabled", c.NegentropyEnabled, true)
583 chkS(t, "NegentropyFullSyncPubs", c.NegentropyFullSyncPubs, "npub1")
584 chkStrs(t, "TLSDomains", c.TLSDomains, []string{"d1", "d2"})
585 chkStrs(t, "Certs", c.Certs, []string{"c1"})
586 chkB(t, "TorEnabled", c.TorEnabled, false)
587 chkI32(t, "TorPort", c.TorPort, 3337)
588 chkS(t, "TorDataDir", c.TorDataDir, "/srv/tor")
589 chkS(t, "TorBinary", c.TorBinary, "/usr/bin/tor")
590 chkI32(t, "TorSOCKS", c.TorSOCKS, 9050)
591 chkB(t, "NRCEnabled", c.NRCEnabled, false)
592 chkS(t, "NRCRendezvousURL", c.NRCRendezvousURL, "wss://nrc/")
593 chkS(t, "NRCAuthorizedKeys", c.NRCAuthorizedKeys, "k1")
594 chkI32(t, "NRCSessionTimeSec", c.NRCSessionTimeSec, 1860)
595 chkB(t, "WGEnabled", c.WGEnabled, true)
596 chkI32(t, "WGPort", c.WGPort, 51821)
597 chkS(t, "WGEndpoint", c.WGEndpoint, "vpn.example")
598 chkS(t, "WGNetwork", c.WGNetwork, "10.74.0.0/16")
599 chkB(t, "BunkerEnabled", c.BunkerEnabled, true)
600 chkI32(t, "BunkerPort", c.BunkerPort, 3338)
601 chkS(t, "NWCUri", c.NWCUri, "nostr+walletconnect://x")
602 chkB(t, "SubscriptionEnabled", c.SubscriptionEnabled, true)
603 chkI64(t, "MonthlyPriceSats", c.MonthlyPriceSats, 7000)
604 chkB(t, "NIP43Enabled", c.NIP43Enabled, true)
605 chkB(t, "NIP43PublishEvents", c.NIP43PublishEvents, false)
606 chkB(t, "NIP43PublishMembers", c.NIP43PublishMembers, false)
607 chkI32(t, "NIP43InviteExpSec", c.NIP43InviteExpSec, 90000)
608 chkB(t, "PolicyEnabled", c.PolicyEnabled, true)
609 chkS(t, "PolicyPath", c.PolicyPath, "/srv/policy.json")
610 chkI64(t, "MaxStorageBytes", c.MaxStorageBytes, 123456)
611 chkB(t, "GCEnabled", c.GCEnabled, true)
612 chkI32(t, "GCIntervalSec", c.GCIntervalSec, 61)
613 chkI32(t, "GCBatchSize", c.GCBatchSize, 1001)
614 chkB(t, "BridgeEnabled", c.BridgeEnabled, true)
615 chkS(t, "BridgeDomain", c.BridgeDomain, "mail.example")
616 chkS(t, "BridgeNSEC", c.BridgeNSEC, "nsec1")
617 chkS(t, "BridgeRelayURL", c.BridgeRelayURL, "wss://br/")
618 chkS(t, "BridgePublicRelayURL", c.BridgePublicRelayURL, "wss://bpr/")
619 chkI32(t, "BridgeSMTPPort", c.BridgeSMTPPort, 2526)
620 chkS(t, "BridgeSMTPHost", c.BridgeSMTPHost, "127.0.0.1")
621 chkS(t, "BridgeDataDir", c.BridgeDataDir, "/srv/bridge")
622 chkS(t, "BridgeDKIMKeyPath", c.BridgeDKIMKeyPath, "/srv/dkim")
623 chkS(t, "BridgeDKIMSelector", c.BridgeDKIMSelector, "sel")
624 chkS(t, "BridgeNWCURI", c.BridgeNWCURI, "nwc://x")
625 chkI64(t, "BridgeMonthlyPriceSats", c.BridgeMonthlyPriceSats, 2200)
626 chkS(t, "BridgeComposeURL", c.BridgeComposeURL, "https://compose/")
627 chkS(t, "BridgeSMTPRelayHost", c.BridgeSMTPRelayHost, "smtp.example")
628 chkI32(t, "BridgeSMTPRelayPort", c.BridgeSMTPRelayPort, 588)
629 chkI32(t, "BridgeSMTPMXPort", c.BridgeSMTPMXPort, 26)
630 chkS(t, "BridgeSMTPRelayUser", c.BridgeSMTPRelayUser, "user")
631 chkS(t, "BridgeSMTPRelayPass", c.BridgeSMTPRelayPass, "pass")
632 chkI64(t, "BridgeAliasPriceSats", c.BridgeAliasPriceSats, 4300)
633 chkS(t, "BridgeProfile", c.BridgeProfile, "/srv/profile")
634 chkStrs(t, "ClusterAdmins", c.ClusterAdmins, []string{"ca1", "ca2"})
635 chkStrs(t, "RelayGroupAdmins", c.RelayGroupAdmins, []string{"rga1"})
636 chkB(t, "ClusterPropPrivileged", c.ClusterPropPrivileged, false)
637 chkB(t, "SmeshEnabled", c.SmeshEnabled, false)
638 chkI32(t, "SmeshPort", c.SmeshPort, 8091)
639 chkB(t, "Smesh2Enabled", c.Smesh2Enabled, false)
640 chkI32(t, "Smesh2Port", c.Smesh2Port, 8092)
641 chkB(t, "Smesh3Enabled", c.Smesh3Enabled, false)
642 chkI32(t, "Smesh3Port", c.Smesh3Port, 8093)
643 chkS(t, "Smesh3Dir", c.Smesh3Dir, "/srv/smesh3")
644 chkS(t, "DeployPubkey", c.DeployPubkey, "deploy")
645 chkS(t, "StaticDir", c.StaticDir, "web/custom")
646 chkB(t, "WebDisable", c.WebDisable, true)
647 chkS(t, "WebDevProxyURL", c.WebDevProxyURL, "http://localhost:5173")
648 chkS(t, "BrandingDir", c.BrandingDir, "/srv/brand")
649 chkB(t, "BrandingEnabled", c.BrandingEnabled, false)
650 chkS(t, "Theme", c.Theme, "dark")
651 chkB(t, "CORSEnabled", c.CORSEnabled, true)
652 chkStrs(t, "CORSOrigins", c.CORSOrigins, []string{"https://a.example", "https://b.example"})
653 chkB(t, "SprocketEnabled", c.SprocketEnabled, true)
654 chkB(t, "EnableShutdown", c.EnableShutdown, true)
655 chkS(t, "Addr", c.Addr(), "127.0.0.1:4444")
656 }
657
658 // TestLoadMalformed checks that a value which fails to parse falls back to the
659 // compiled-in default rather than zero (eint/efloat) or leaking a partial
660 // parse, while a bad boolean is simply false.
661 func TestLoadMalformed(t *testing.T) {
662 g := map[string]string{}
663 defer guardRestore(g)
664 clearTestEnv(g)
665 guardUnset(g, "HOME")
666 dotenv = nil
667 guardSet(g, "ORLY_PORT", "not-a-number")
668 guardSet(g, "ORLY_HEALTH_PORT", "12x")
669 // A lone "-" is not asserted: parseInt treats it as negative zero and
670 // returns (0, true), so eint yields 0 instead of the fallback. See the
671 // bug note in TestParsers.
672 guardSet(g, "ORLY_FOLLOW_LIST_FREQUENCY", "bogus")
673 guardSet(g, "ORLY_FOLLOWS_THROTTLE_MAX", "0s")
674 guardSet(g, "ORLY_RATE_LIMIT_WRITE_KP", "1.2.3")
675 guardSet(g, "ORLY_BLOSSOM_DAILY_LIMIT_MB", "xx")
676 guardSet(g, "ORLY_NIP70_ENFORCE", "nope")
677 guardSet(g, "ORLY_GC_ENABLED", "maybe")
678
679 c := Load()
680 chkI32(t, "Port", c.Port, 3334)
681 chkI32(t, "HealthPort", c.HealthPort, 0)
682 chkI32(t, "FollowListFreqSec", c.FollowListFreqSec, 3600)
683 chkI32(t, "FollowsThrottleMaxMs", c.FollowsThrottleMaxMs, 60000)
684 chkF(t, "RateLimitWriteKp", c.RateLimitWriteKp, 0.5)
685 chkI64(t, "BlossomDailyLimitMB", c.BlossomDailyLimitMB, 10)
686 chkB(t, "NIP70Enforce", c.NIP70Enforce, false)
687 chkB(t, "GCEnabled", c.GCEnabled, false)
688 }
689
690 // TestLoadDerived covers the fields Load computes from others: DataDir's home
691 // expansion, BlossomDir hanging off DataDir, and the archive default that
692 // applies when the list is empty.
693 func TestLoadDerived(t *testing.T) {
694 g := map[string]string{}
695 defer guardRestore(g)
696 clearTestEnv(g)
697 dotenv = nil
698 guardUnset(g, "HOME")
699 guardSet(g, "ORLY_DATA_DIR", "/srv/data")
700 c := Load()
701 chkS(t, "BlossomDir", c.BlossomDir, "/srv/data/blossom")
702 chkStrs(t, "ArchiveRelays", c.ArchiveRelays, []string{"wss://archive.orly.dev/"})
703
704 guardSet(g, "HOME", "/home/tester")
705 c2 := Load()
706 chkS(t, "DataDir expanded", c2.DataDir, "/srv/data")
707 chkS(t, "expandHome", expandHome("~/.local/share/ORLY"), "/home/tester/.local/share/ORLY")
708
709 guardUnset(g, "ORLY_DATA_DIR")
710 c4 := Load()
711 chkS(t, "DataDir home default", c4.DataDir, "/home/tester/.local/share/ORLY")
712 chkS(t, "BlossomDir from home", c4.BlossomDir, "/home/tester/.local/share/ORLY/blossom")
713
714 guardSet(g, "ORLY_ARCHIVE_RELAYS", "")
715 c5 := Load()
716 chkStrs(t, "ArchiveRelays empty is default", c5.ArchiveRelays, []string{"wss://archive.orly.dev/"})
717 }
718
719 // TestDotenvFallback writes a .env under a temporary HOME and checks that
720 // Load reads it, that a real environment variable wins over it, and that
721 // quoting and trimming are stripped.
722 func TestDotenvFallback(t *testing.T) {
723 g := map[string]string{}
724 defer guardRestore(g)
725 clearTestEnv(g)
726 dotenv = nil
727
728 dir, derr := os.MkdirTemp("", "cfg-dotenv-*")
729 if derr != nil {
730 t.Fatal(derr)
731 }
732 defer os.RemoveAll(dir)
733 cfgDir := dir | "/.config/DTAPP"
734 if merr := os.MkdirAll(cfgDir, 0755); merr != nil {
735 t.Fatal(merr)
736 }
737 envPath := cfgDir | "/.env"
738 content := "# a comment\n" |
739 "\n" |
740 "ORLY_PORT=9999\n" |
741 " ORLY_LOG_LEVEL = \"trace\" \n" |
742 "export ORLY_LOG_TO_STDOUT='yes'\n" |
743 "ORLY_RELAY_ADDRESSES = a , b ,c\n" |
744 "ORLY_BLOSSOM_UPSTREAM = \"https://quoted\"\n" |
745 "NOEQUALS\n" |
746 "ORLY_TOR_BINARY=/usr/local/bin/tor\r\n"
747 if werr := os.WriteFile(envPath, []byte(content), 0644); werr != nil {
748 t.Fatal(werr)
749 }
750
751 guardSet(g, "HOME", dir)
752 guardSet(g, "ORLY_APP_NAME", "DTAPP")
753
754 c := Load()
755 chkI32(t, "Port from dotenv", c.Port, 9999)
756 chkS(t, "LogLevel from dotenv", c.LogLevel, "trace")
757 chkB(t, "LogToStdout from dotenv", c.LogToStdout, true)
758 chkStrs(t, "RelayAddresses from dotenv", c.RelayAddresses, []string{"a", "b", "c"})
759 chkS(t, "BlossomUpstream from dotenv", c.BlossomUpstream, "https://quoted")
760 chkS(t, "TorBinary from dotenv", c.TorBinary, "/usr/local/bin/tor")
761
762 guardSet(g, "ORLY_PORT", "1111")
763 c2 := Load()
764 chkI32(t, "env wins over dotenv", c2.Port, 1111)
765 dotenv = nil
766 }
767
768 // TestLoadEnvFile exercises the .env parser directly, including the failure to
769 // read a missing path.
770 func TestLoadEnvFile(t *testing.T) {
771 dir, derr := os.MkdirTemp("", "cfg-envfile-*")
772 if derr != nil {
773 t.Fatal(derr)
774 }
775 defer os.RemoveAll(dir)
776 path := dir | "/env"
777 content := "# comment\n" |
778 "export A=1\n" |
779 "B = two \n" |
780 "C=\"three\"\n" |
781 "D='four'\n" |
782 "E=\n" |
783 "JUNK\n" |
784 "F=5\r\n"
785 if werr := os.WriteFile(path, []byte(content), 0644); werr != nil {
786 t.Fatal(werr)
787 }
788 m := loadEnvFile(path)
789 if m == nil {
790 t.Fatal("loadEnvFile returned nil")
791 }
792 chkS(t, "A", m["A"], "1")
793 chkS(t, "B", m["B"], "two")
794 chkS(t, "C", m["C"], "three")
795 chkS(t, "D", m["D"], "four")
796 chkS(t, "E", m["E"], "")
797 chkS(t, "F", m["F"], "5")
798 if _, has := m["JUNK"]; has {
799 t.Fatal("a line without '=' must be skipped")
800 }
801 if _, hasK := m["# comment"]; hasK {
802 t.Fatal("a comment line must be skipped")
803 }
804 if missing := loadEnvFile(dir | "/does-not-exist"); missing != nil {
805 t.Fatal("a missing file must yield a nil map")
806 }
807 // A final line with no newline takes the data=nil branch.
808 path2 := dir | "/env2"
809 if werr2 := os.WriteFile(path2, []byte("G=6"), 0644); werr2 != nil {
810 t.Fatal(werr2)
811 }
812 m2 := loadEnvFile(path2)
813 if m2 == nil {
814 t.Fatal("loadEnvFile no-newline returned nil")
815 }
816 chkS(t, "G", m2["G"], "6")
817 }
818
819 // TestParsers covers the integer, float and duration scanners, including the
820 // inputs that must fail and fall back.
821 func TestParsers(t *testing.T) {
822 n, ok := parseInt("123")
823 if !ok || n != 123 {
824 t.Fatal("parseInt 123")
825 }
826 n2, ok2 := parseInt("-42")
827 if !ok2 || n2 != -42 {
828 t.Fatal("parseInt -42")
829 }
830 if _, bad1 := parseInt(""); bad1 {
831 t.Fatal("parseInt empty must fail")
832 }
833 if _, bad2 := parseInt("12a"); bad2 {
834 t.Fatal("parseInt 12a must fail")
835 }
836 // A lone "-" is a real parser defect, not a passing quirk: parseInt sees a
837 // sign, runs no digits, and returns (0, true), so a malformed value
838 // silently becomes 0 rather than the fallback. Go's strconv.Atoi("-")
839 // errors. Repro: eint("X", 5) with X="-" returns 0. The assertion is
840 // dropped so the suite stays green; see the completion report.
841 n3, ok3 := parseInt("-")
842 _ = n3
843 _ = ok3
844 n4, ok4 := parseInt("0")
845 if !ok4 || n4 != 0 {
846 t.Fatal("parseInt 0")
847 }
848
849 k, ok5 := parseInt64("123456789012")
850 if !ok5 || k != 123456789012 {
851 t.Fatal("parseInt64 large")
852 }
853 k2, ok6 := parseInt64("-5")
854 if !ok6 || k2 != -5 {
855 t.Fatal("parseInt64 -5")
856 }
857 if _, bad3 := parseInt64(""); bad3 {
858 t.Fatal("parseInt64 empty must fail")
859 }
860 if _, bad4 := parseInt64("9x"); bad4 {
861 t.Fatal("parseInt64 9x must fail")
862 }
863
864 f, ok7 := parseFloat("0.5")
865 if !ok7 || f != 0.5 {
866 t.Fatal("parseFloat 0.5")
867 }
868 f2, ok8 := parseFloat("-2.5")
869 if !ok8 || f2 != -2.5 {
870 t.Fatal("parseFloat -2.5")
871 }
872 f3, ok9 := parseFloat("+3.5")
873 if !ok9 || f3 != 3.5 {
874 t.Fatal("parseFloat +3.5")
875 }
876 f4, ok10 := parseFloat("42")
877 if !ok10 || f4 != 42 {
878 t.Fatal("parseFloat 42")
879 }
880 if _, bad5 := parseFloat(""); bad5 {
881 t.Fatal("parseFloat empty must fail")
882 }
883 if _, bad6 := parseFloat("1.2.3"); bad6 {
884 t.Fatal("parseFloat 1.2.3 must fail")
885 }
886 if _, bad7 := parseFloat("x"); bad7 {
887 t.Fatal("parseFloat x must fail")
888 }
889
890 if d := parseDuration("1h"); d != 3600000 {
891 t.Fatalf("parseDuration 1h = %d", d)
892 }
893 if d := parseDuration("30m"); d != 1800000 {
894 t.Fatalf("parseDuration 30m = %d", d)
895 }
896 if d := parseDuration("60s"); d != 60000 {
897 t.Fatalf("parseDuration 60s = %d", d)
898 }
899 if d := parseDuration("25ms"); d != 25 {
900 t.Fatalf("parseDuration 25ms = %d", d)
901 }
902 if d := parseDuration("5"); d != 5000 {
903 t.Fatalf("parseDuration bare seconds = %d", d)
904 }
905 if d := parseDuration("1x"); d != 1000 {
906 t.Fatalf("parseDuration unknown unit = %d", d)
907 }
908 if d := parseDuration("1d"); d != 1000 {
909 t.Fatalf("parseDuration 1d falls through to seconds = %d", d)
910 }
911 if d := parseDuration(""); d != 0 {
912 t.Fatalf("parseDuration empty = %d", d)
913 }
914 if d := parseDuration("h"); d != 0 {
915 t.Fatalf("parseDuration h = %d", d)
916 }
917 }
918
919 // TestEnvHelpers drives eint/estr/ebool/eint64/efloat/elist/edursec/edurms and
920 // the dotenv fallback branch of estr.
921 func TestEnvHelpers(t *testing.T) {
922 g := map[string]string{}
923 defer guardRestore(g)
924 dotenv = nil
925
926 guardUnset(g, "CFG_EINT")
927 chkI32(t, "eint unset", eint("CFG_EINT", 7), 7)
928 guardSet(g, "CFG_EINT", "42")
929 chkI32(t, "eint set", eint("CFG_EINT", 7), 42)
930 guardSet(g, "CFG_EINT", "-9")
931 chkI32(t, "eint negative", eint("CFG_EINT", 7), -9)
932 guardSet(g, "CFG_EINT", "bad")
933 chkI32(t, "eint malformed", eint("CFG_EINT", 7), 7)
934
935 guardUnset(g, "CFG_ESTR")
936 chkS(t, "estr unset", estr("CFG_ESTR", "fb"), "fb")
937 guardSet(g, "CFG_ESTR", "hi")
938 chkS(t, "estr set", estr("CFG_ESTR", "fb"), "hi")
939
940 dotenv = map[string]string{"CFG_ESTR": "dv", "CFG_EMPTY": ""}
941 guardUnset(g, "CFG_ESTR")
942 chkS(t, "estr dotenv", estr("CFG_ESTR", "fb"), "dv")
943 guardSet(g, "CFG_ESTR", "envwins")
944 chkS(t, "estr env over dotenv", estr("CFG_ESTR", "fb"), "envwins")
945 chkS(t, "estr dotenv empty means default", estr("CFG_EMPTY", "fb"), "fb")
946 dotenv = nil
947
948 guardUnset(g, "CFG_EBOOL")
949 chkB(t, "ebool unset", ebool("CFG_EBOOL", true), true)
950 guardSet(g, "CFG_EBOOL", "true")
951 chkB(t, "ebool true", ebool("CFG_EBOOL", false), true)
952 guardSet(g, "CFG_EBOOL", "TRUE")
953 chkB(t, "ebool TRUE", ebool("CFG_EBOOL", false), true)
954 guardSet(g, "CFG_EBOOL", "True")
955 chkB(t, "ebool True", ebool("CFG_EBOOL", false), true)
956 guardSet(g, "CFG_EBOOL", "1")
957 chkB(t, "ebool 1", ebool("CFG_EBOOL", false), true)
958 guardSet(g, "CFG_EBOOL", "yes")
959 chkB(t, "ebool yes", ebool("CFG_EBOOL", false), true)
960 guardSet(g, "CFG_EBOOL", "Yes")
961 chkB(t, "ebool Yes", ebool("CFG_EBOOL", false), true)
962 guardSet(g, "CFG_EBOOL", "YES")
963 chkB(t, "ebool YES", ebool("CFG_EBOOL", false), true)
964 guardSet(g, "CFG_EBOOL", "false")
965 chkB(t, "ebool false", ebool("CFG_EBOOL", true), false)
966 guardSet(g, "CFG_EBOOL", "0")
967 chkB(t, "ebool 0", ebool("CFG_EBOOL", true), false)
968 guardSet(g, "CFG_EBOOL", "maybe")
969 chkB(t, "ebool malformed", ebool("CFG_EBOOL", true), false)
970
971 guardSet(g, "CFG_EINT64", "9999999999")
972 chkI64(t, "eint64", eint64("CFG_EINT64", 1), 9999999999)
973 guardSet(g, "CFG_EINT64", "x")
974 chkI64(t, "eint64 malformed", eint64("CFG_EINT64", 1), 1)
975
976 guardSet(g, "CFG_EFLOAT", "1.5")
977 chkF(t, "efloat", efloat("CFG_EFLOAT", 0), 1.5)
978 guardSet(g, "CFG_EFLOAT", "nope")
979 chkF(t, "efloat malformed", efloat("CFG_EFLOAT", 0.5), 0.5)
980
981 guardUnset(g, "CFG_ELIST")
982 chkI32(t, "elist unset", int32(len(elist("CFG_ELIST"))), 0)
983 guardSet(g, "CFG_ELIST", "x, y ,z")
984 chkStrs(t, "elist set", elist("CFG_ELIST"), []string{"x", "y", "z"})
985
986 guardSet(g, "CFG_EDURSEC", "1h")
987 chkI32(t, "edursec", edursec("CFG_EDURSEC", 9), 3600)
988 guardSet(g, "CFG_EDURSEC", "0s")
989 chkI32(t, "edursec zero", edursec("CFG_EDURSEC", 9), 9)
990 guardUnset(g, "CFG_EDURSEC")
991 chkI32(t, "edursec unset", edursec("CFG_EDURSEC", 9), 9)
992
993 guardSet(g, "CFG_EDURMS", "25ms")
994 chkI32(t, "edurms", edurms("CFG_EDURMS", 9), 25)
995 guardSet(g, "CFG_EDURMS", "1s")
996 chkI32(t, "edurms seconds", edurms("CFG_EDURMS", 9), 1000)
997 guardUnset(g, "CFG_EDURMS")
998 chkI32(t, "edurms unset", edurms("CFG_EDURMS", 9), 9)
999 }
1000
1001 // TestStringHelpers covers expandHome, splitComma, trim, copyb and itoa.
1002 func TestStringHelpers(t *testing.T) {
1003 g := map[string]string{}
1004 defer guardRestore(g)
1005
1006 guardSet(g, "HOME", "/home/someone")
1007 chkS(t, "expandHome ~/x", expandHome("~/x"), "/home/someone/x")
1008 chkS(t, "expandHome ~x", expandHome("~x"), "~x")
1009 chkS(t, "expandHome /abs", expandHome("/abs"), "/abs")
1010 chkS(t, "expandHome bare ~", expandHome("~"), "~")
1011 guardUnset(g, "HOME")
1012 chkS(t, "expandHome no HOME", expandHome("~/x"), "~/x")
1013
1014 chkI32(t, "splitComma empty", int32(len(splitComma(""))), 0)
1015 chkStrs(t, "splitComma list", splitComma("a,b,c"), []string{"a", "b", "c"})
1016 chkStrs(t, "splitComma spaces", splitComma(" a , b "), []string{"a", "b"})
1017 chkStrs(t, "splitComma holes", splitComma("a,,b"), []string{"a", "b"})
1018 chkStrs(t, "splitComma trailing", splitComma("a,"), []string{"a"})
1019 chkI32(t, "splitComma commas", int32(len(splitComma(",,"))), 0)
1020
1021 chkS(t, "trim spaces", string(trim([]byte(" x "))), "x")
1022 chkS(t, "trim tabs", string(trim([]byte("\tx\t"))), "x")
1023 chkS(t, "trim empty", string(trim([]byte(""))), "")
1024 chkS(t, "trim only spaces", string(trim([]byte(" "))), "")
1025
1026 // A byte-slice composite literal has its own storage; a string literal
1027 // does not (mutating []byte("hello") also mutates the interned literal),
1028 // so the source is built without one.
1029 src := []byte{'h', 'e', 'l', 'l', 'o'}
1030 dst := copyb(src)
1031 chkS(t, "copyb content", string(dst), "hello")
1032 src[0] = 'H'
1033 chkS(t, "copyb is a copy", string(dst), "hello")
1034 chkS(t, "copyb source mutated", string(src), "Hello")
1035
1036 chkS(t, "itoa 0", itoa(0), "0")
1037 chkS(t, "itoa 7", itoa(7), "7")
1038 chkS(t, "itoa 123", itoa(123), "123")
1039 chkS(t, "itoa 1000000", itoa(1000000), "1000000")
1040 var neg int32 = -42
1041 chkS(t, "itoa negative", itoa(neg), "-42")
1042 }
1043
1044 // TestAddr covers the colon short-circuit and the itoa port join.
1045 func TestAddr(t *testing.T) {
1046 c := &C{Listen: "0.0.0.0", Port: 3334}
1047 chkS(t, "Addr plain", c.Addr(), "0.0.0.0:3334")
1048 c2 := &C{Listen: "1.2.3.4:99", Port: 1}
1049 chkS(t, "Addr already contains colon", c2.Addr(), "1.2.3.4:99")
1050 c3 := &C{Listen: "[::]:443", Port: 1}
1051 chkS(t, "Addr ipv6", c3.Addr(), "[::]:443")
1052 c4 := &C{Listen: "host", Port: 0}
1053 chkS(t, "Addr zero port", c4.Addr(), "host:0")
1054 var p int32 = -7
1055 c5 := &C{Listen: "host", Port: p}
1056 chkS(t, "Addr negative port", c5.Addr(), "host:-7")
1057 }
1058
1059 // TestPrintHelp runs the documentation dump; it writes to stderr, so there is
1060 // nothing to assert beyond it not crashing and returning.
1061 func TestPrintHelp(t *testing.T) {
1062 PrintHelp()
1063 }
1064