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