// Package config tests drive Load through a controlled environment. Every knob // Load reads is named in testEnv with a non-default value; TestLoadDefaults // clears the same set to assert the defaults. Helpers are exercised directly so // the parse/trim/duration branches are covered without a whole Load. // // Environment mutations are captured in a map and restored by a deferred free // function: a deferred closure cannot capture the map, and the harness has no // t.Setenv. package config import ( "os" "testing" ) // testEnv is every ORLY_* variable Load reads, with a non-default value. var testEnv = map[string]string{ "ORLY_APP_NAME": "SMESH", "ORLY_DATA_DIR": "/srv/smesh", "ORLY_LISTEN": "127.0.0.1", "ORLY_PORT": "4444", "ORLY_HEALTH_PORT": "8888", "ORLY_LOG_LEVEL": "debug", "ORLY_LOG_TO_STDOUT": "true", "ORLY_LOG_BUFFER_SIZE": "777", "ORLY_RELAY_URL": "wss://relay.example.com", "ORLY_RELAY_ADDRESSES": "a,b,c", "ORLY_RELAY_PEERS": "p1,p2", "ORLY_SYNC_PUBKEY": "deadbeef", "ORLY_CLIENT_TAG": "mytag", "ORLY_AUTH_REQUIRED": "true", "ORLY_AUTH_TO_WRITE": "true", "ORLY_PRIVILEGED_OPEN": "true", "ORLY_NIP70_ENFORCE": "false", "ORLY_MARMOT_OPEN": "true", "ORLY_NIP46_BYPASS_AUTH": "true", "ORLY_ACL_MODE": "managed", "ORLY_MUTE_BLACKLIST": "beef", "ORLY_ADMINS": "adm1,adm2", "ORLY_OWNERS": "own1", "ORLY_FREE_WRITE_LIMIT": "5", "ORLY_FREE_WRITE_WINDOW": "60", "ORLY_MAX_CONN_PER_IP": "11", "ORLY_INGEST_WORKERS": "3", "ORLY_MEDIA_PROXY_WORKERS": "9", "ORLY_BLOSSOM_WORKERS": "5", "ORLY_MAX_GLOBAL_CONNECTIONS": "77", "ORLY_MAX_SUBSCRIPTIONS": "88", "ORLY_CONN_DELAY_MAX_MS": "123", "ORLY_IP_WHITELIST": "10.0.0.0/8,192.168.0.1", "ORLY_IP_BLACKLIST": "1.2.3.4", "ORLY_HTTP_GUARD_BOT_BLOCK": "false", "ORLY_RATE_LIMIT_ENABLED": "false", "ORLY_RATE_LIMIT_TARGET_MB": "9", "ORLY_RATE_LIMIT_WRITE_KP": "1.5", "ORLY_RATE_LIMIT_WRITE_KI": "0.25", "ORLY_RATE_LIMIT_WRITE_KD": "0.125", "ORLY_RATE_LIMIT_READ_KP": "0.75", "ORLY_RATE_LIMIT_READ_KI": "0.0625", "ORLY_RATE_LIMIT_READ_KD": "0.03125", "ORLY_RATE_LIMIT_MAX_WRITE_MS": "1500", "ORLY_RATE_LIMIT_MAX_READ_MS": "600", "ORLY_RATE_LIMIT_WRITE_TARGET": "0.875", "ORLY_RATE_LIMIT_READ_TARGET": "0.625", "ORLY_RATE_LIMIT_EMERGENCY_THRESHOLD": "1.5", "ORLY_RATE_LIMIT_RECOVERY_THRESHOLD": "0.25", "ORLY_RATE_LIMIT_EMERGENCY_MAX_MS": "6000", "ORLY_QUERY_RESULT_LIMIT": "512", "ORLY_FOLLOW_LIST_FREQUENCY": "2h", "ORLY_FOLLOWS_THROTTLE": "true", "ORLY_FOLLOWS_THROTTLE_INCREMENT": "30ms", "ORLY_FOLLOWS_THROTTLE_MAX": "90s", "ORLY_SOCIAL_THROTTLE_D2_INCREMENT": "55ms", "ORLY_SOCIAL_THROTTLE_D2_MAX": "31s", "ORLY_SOCIAL_THROTTLE_D3_INCREMENT": "210ms", "ORLY_SOCIAL_THROTTLE_D3_MAX": "61s", "ORLY_SOCIAL_THROTTLE_OUTSIDER_INCREMENT": "510ms", "ORLY_SOCIAL_THROTTLE_OUTSIDER_MAX": "121s", "ORLY_SOCIAL_WOT_DEPTH": "4", "ORLY_SOCIAL_WOT_REFRESH": "2h", "ORLY_GRAPEVINE_ENABLED": "true", "ORLY_GRAPEVINE_MAX_DEPTH": "7", "ORLY_GRAPEVINE_MAX_CYCLES": "21", "ORLY_GRAPEVINE_ATTENUATION": "0.5", "ORLY_GRAPEVINE_RIGOR": "0.75", "ORLY_GRAPEVINE_FOLLOW_CONFIDENCE": "0.125", "ORLY_GRAPEVINE_OBSERVERS": "obs1,obs2", "ORLY_GRAPEVINE_REFRESH": "7h", "ORLY_GRAPEVINE_AUTO_WHITELIST": "true", "ORLY_GRAPEVINE_WHITELIST_THRESHOLD": "0.375", "ORLY_GRAPEVINE_WHITELIST_REFRESH": "8h", "ORLY_GRAPH_QUERIES_ENABLED": "false", "ORLY_GRAPH_MAX_DEPTH": "17", "ORLY_GRAPH_MAX_RESULTS": "20000", "ORLY_GRAPH_RATE_LIMIT_RPM": "61", "ORLY_PROXY_ENABLED": "false", "ORLY_PROXY_MAX_RELAYS": "17", "ORLY_PROXY_TIMEOUT_SEC": "16", "ORLY_ARCHIVE_ENABLED": "true", "ORLY_ARCHIVE_RELAYS": "wss://arch.example/", "ORLY_ARCHIVE_TIMEOUT_SEC": "31", "ORLY_ARCHIVE_CACHE_TTL_HRS": "25", "ORLY_BLOSSOM_ENABLED": "false", "ORLY_BLOSSOM_DIR": "/srv/blobs", "ORLY_BLOSSOM_UPSTREAM": "https://up.example", "ORLY_BLOSSOM_SERVICE_LEVELS": "s1:1", "ORLY_BLOSSOM_RATE_LIMIT": "true", "ORLY_BLOSSOM_DAILY_LIMIT_MB": "11", "ORLY_BLOSSOM_BURST_LIMIT_MB": "51", "ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG": "true", "ORLY_BOOTSTRAP_RELAYS": "wss://b1/", "ORLY_SPIDER_MODE": "follows", "ORLY_DIRECTORY_SPIDER": "true", "ORLY_DIRECTORY_SPIDER_INTERVAL": "25h", "ORLY_DIRECTORY_SPIDER_HOPS": "4", "ORLY_CRAWLER_ENABLED": "true", "ORLY_CRAWLER_DISCOVERY_INTERVAL": "5h", "ORLY_CRAWLER_SYNC_INTERVAL": "31m", "ORLY_CRAWLER_MAX_HOPS": "6", "ORLY_CRAWLER_CONCURRENCY": "4", "ORLY_NEGENTROPY_ENABLED": "true", "ORLY_NEGENTROPY_FULL_SYNC_PUBKEYS": "npub1", "ORLY_TLS_DOMAINS": "d1,d2", "ORLY_CERTS": "c1", "ORLY_TOR_ENABLED": "false", "ORLY_TOR_PORT": "3337", "ORLY_TOR_DATA_DIR": "/srv/tor", "ORLY_TOR_BINARY": "/usr/bin/tor", "ORLY_TOR_SOCKS": "9050", "ORLY_NRC_ENABLED": "false", "ORLY_NRC_RENDEZVOUS_URL": "wss://nrc/", "ORLY_NRC_AUTHORIZED_KEYS": "k1", "ORLY_NRC_SESSION_TIMEOUT": "31m", "ORLY_WG_ENABLED": "true", "ORLY_WG_PORT": "51821", "ORLY_WG_ENDPOINT": "vpn.example", "ORLY_WG_NETWORK": "10.74.0.0/16", "ORLY_BUNKER_ENABLED": "true", "ORLY_BUNKER_PORT": "3338", "ORLY_NWC_URI": "nostr+walletconnect://x", "ORLY_SUBSCRIPTION_ENABLED": "true", "ORLY_MONTHLY_PRICE_SATS": "7000", "ORLY_NIP43_ENABLED": "true", "ORLY_NIP43_PUBLISH_EVENTS": "false", "ORLY_NIP43_PUBLISH_MEMBER_LIST": "false", "ORLY_NIP43_INVITE_EXPIRY": "25h", "ORLY_POLICY_ENABLED": "true", "ORLY_POLICY_PATH": "/srv/policy.json", "ORLY_MAX_STORAGE_BYTES": "123456", "ORLY_GC_ENABLED": "true", "ORLY_GC_INTERVAL_SEC": "61", "ORLY_GC_BATCH_SIZE": "1001", "ORLY_BRIDGE_ENABLED": "true", "ORLY_BRIDGE_DOMAIN": "mail.example", "ORLY_BRIDGE_NSEC": "nsec1", "ORLY_BRIDGE_RELAY_URL": "wss://br/", "ORLY_BRIDGE_PUBLIC_RELAY_URL": "wss://bpr/", "ORLY_BRIDGE_SMTP_PORT": "2526", "ORLY_BRIDGE_SMTP_HOST": "127.0.0.1", "ORLY_BRIDGE_DATA_DIR": "/srv/bridge", "ORLY_BRIDGE_DKIM_KEY": "/srv/dkim", "ORLY_BRIDGE_DKIM_SELECTOR": "sel", "ORLY_BRIDGE_NWC_URI": "nwc://x", "ORLY_BRIDGE_MONTHLY_PRICE_SATS": "2200", "ORLY_BRIDGE_COMPOSE_URL": "https://compose/", "ORLY_BRIDGE_SMTP_RELAY_HOST": "smtp.example", "ORLY_BRIDGE_SMTP_RELAY_PORT": "588", "ORLY_BRIDGE_SMTP_MX_PORT": "26", "ORLY_BRIDGE_SMTP_RELAY_USERNAME": "user", "ORLY_BRIDGE_SMTP_RELAY_PASSWORD": "pass", "ORLY_BRIDGE_ALIAS_PRICE_SATS": "4300", "ORLY_BRIDGE_PROFILE": "/srv/profile", "ORLY_CLUSTER_ADMINS": "ca1,ca2", "ORLY_RELAY_GROUP_ADMINS": "rga1", "ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS": "false", "ORLY_SMESH_ENABLED": "false", "ORLY_SMESH_PORT": "8091", "ORLY_SMESH2_ENABLED": "false", "ORLY_SMESH2_PORT": "8092", "ORLY_SMESH3_ENABLED": "false", "ORLY_SMESH3_PORT": "8093", "ORLY_SMESH3_DIR": "/srv/smesh3", "ORLY_DEPLOY_PUBKEY": "deploy", "ORLY_STATIC_DIR": "web/custom", "ORLY_WEB_DISABLE": "true", "ORLY_WEB_DEV_PROXY_URL": "http://localhost:5173", "ORLY_BRANDING_DIR": "/srv/brand", "ORLY_BRANDING_ENABLED": "false", "ORLY_THEME": "dark", "ORLY_CORS_ENABLED": "true", "ORLY_CORS_ORIGINS": "https://a.example,https://b.example", "ORLY_SPROCKET_ENABLED": "true", "ORLY_ENABLE_SHUTDOWN": "true", } func guardSet(g map[string]string, k, v string) { g[k] = os.Getenv(k) os.Setenv(k, v) } func guardUnset(g map[string]string, k string) { g[k] = os.Getenv(k) os.Unsetenv(k) } func guardRestore(g map[string]string) { for k, v := range g { if v == "" { os.Unsetenv(k) } else { os.Setenv(k, v) } } } func clearTestEnv(g map[string]string) { for k := range testEnv { guardUnset(g, k) } } func setTestEnv(g map[string]string) { for k, v := range testEnv { guardSet(g, k, v) } } func chkI32(t *testing.T, name string, got, want int32) { if got != want { t.Fatalf("%s = %d, want %d", name, got, want) } } func chkI64(t *testing.T, name string, got, want int64) { if got != want { t.Fatalf("%s = %d, want %d", name, got, want) } } func chkF(t *testing.T, name string, got, want float64) { d := got - want if d < 0 { d = -d } if d > 0.000001 { t.Fatalf("%s = %f, want %f", name, got, want) } } func chkS(t *testing.T, name, got, want string) { if got != want { t.Fatalf("%s = %s, want %s", name, got, want) } } func chkB(t *testing.T, name string, got, want bool) { if got != want { if want { t.Fatalf("%s = false, want true", name) } t.Fatalf("%s = true, want false", name) } } func chkStrs(t *testing.T, name string, got []string, want []string) { if len(got) != len(want) { t.Fatalf("%s len = %d, want %d", name, int32(len(got)), int32(len(want))) } for i := 0; i < len(got); i++ { if got[i] != want[i] { t.Fatalf("%s[%d] = %s, want %s", name, i, got[i], want[i]) } } } // TestLoadDefaults clears every knob and asserts what Load produces from an // empty environment, including the two derived defaults (ArchiveRelays and // BlossomDir). func TestLoadDefaults(t *testing.T) { g := map[string]string{} defer guardRestore(g) clearTestEnv(g) guardUnset(g, "HOME") dotenv = nil c := Load() if c == nil { t.Fatal("Load returned nil") } chkS(t, "AppName", c.AppName, "ORLY") chkS(t, "DataDir", c.DataDir, "~/.local/share/ORLY") chkS(t, "Listen", c.Listen, "0.0.0.0") chkI32(t, "Port", c.Port, 3334) chkI32(t, "HealthPort", c.HealthPort, 0) chkS(t, "LogLevel", c.LogLevel, "info") chkB(t, "LogToStdout", c.LogToStdout, false) chkI32(t, "LogBufferSize", c.LogBufferSize, 10000) chkS(t, "RelayURL", c.RelayURL, "") chkI32(t, "RelayAddresses len", int32(len(c.RelayAddresses)), 0) chkI32(t, "RelayPeers len", int32(len(c.RelayPeers)), 0) chkS(t, "SyncPubkey", c.SyncPubkey, "") chkS(t, "ClientTag", c.ClientTag, "git.smesh.lol/smesh") chkB(t, "AuthRequired", c.AuthRequired, false) chkB(t, "AuthToWrite", c.AuthToWrite, false) chkB(t, "PrivilegedOpen", c.PrivilegedOpen, false) chkB(t, "NIP70Enforce", c.NIP70Enforce, true) chkB(t, "MarmotOpen", c.MarmotOpen, false) chkB(t, "NIP46BypassAuth", c.NIP46BypassAuth, false) chkS(t, "ACLMode", c.ACLMode, "none") chkS(t, "MuteBlacklist", c.MuteBlacklist, "") chkI32(t, "Admins len", int32(len(c.Admins)), 0) chkI32(t, "Owners len", int32(len(c.Owners)), 0) chkI32(t, "FreeWriteLimit", c.FreeWriteLimit, 25) chkI32(t, "FreeWriteWindow", c.FreeWriteWindow, 300) chkI32(t, "MaxConnPerIP", c.MaxConnPerIP, 100) chkI32(t, "IngestWorkers", c.IngestWorkers, 0) chkI32(t, "MediaProxyWorkers", c.MediaProxyWorkers, 8) chkI32(t, "BlossomWorkers", c.BlossomWorkers, 4) chkI32(t, "MaxGlobalConns", c.MaxGlobalConns, 500) chkI32(t, "MaxSubscriptions", c.MaxSubscriptions, 10000) chkI32(t, "ConnDelayMaxMs", c.ConnDelayMaxMs, 2000) chkI32(t, "IPWhitelist len", int32(len(c.IPWhitelist)), 0) chkI32(t, "IPBlacklist len", int32(len(c.IPBlacklist)), 0) chkB(t, "HTTPGuardBotBlock", c.HTTPGuardBotBlock, true) chkB(t, "RateLimitEnabled", c.RateLimitEnabled, true) chkI32(t, "RateLimitTargetMB", c.RateLimitTargetMB, 0) chkF(t, "RateLimitWriteKp", c.RateLimitWriteKp, 0.5) chkF(t, "RateLimitWriteKi", c.RateLimitWriteKi, 0.1) chkF(t, "RateLimitWriteKd", c.RateLimitWriteKd, 0.05) chkF(t, "RateLimitReadKp", c.RateLimitReadKp, 0.3) chkF(t, "RateLimitReadKi", c.RateLimitReadKi, 0.05) chkF(t, "RateLimitReadKd", c.RateLimitReadKd, 0.02) chkI32(t, "RateLimitMaxWriteMs", c.RateLimitMaxWriteMs, 1000) chkI32(t, "RateLimitMaxReadMs", c.RateLimitMaxReadMs, 500) chkF(t, "RateLimitWriteTarget", c.RateLimitWriteTarget, 0.85) chkF(t, "RateLimitReadTarget", c.RateLimitReadTarget, 0.90) chkF(t, "EmergencyThreshold", c.EmergencyThreshold, 1.167) chkF(t, "RecoveryThreshold", c.RecoveryThreshold, 0.833) chkI32(t, "EmergencyMaxMs", c.EmergencyMaxMs, 5000) chkI32(t, "QueryResultLimit", c.QueryResultLimit, 256) chkI32(t, "FollowListFreqSec", c.FollowListFreqSec, 3600) chkB(t, "FollowsThrottle", c.FollowsThrottle, false) chkI32(t, "FollowsThrottleIncrMs", c.FollowsThrottleIncrMs, 25) chkI32(t, "FollowsThrottleMaxMs", c.FollowsThrottleMaxMs, 60000) chkI32(t, "SocialThrottleD2IncrMs", c.SocialThrottleD2IncrMs, 50) chkI32(t, "SocialThrottleD2MaxMs", c.SocialThrottleD2MaxMs, 30000) chkI32(t, "SocialThrottleD3IncrMs", c.SocialThrottleD3IncrMs, 200) chkI32(t, "SocialThrottleD3MaxMs", c.SocialThrottleD3MaxMs, 60000) chkI32(t, "SocialThrottleOutsiderIncrMs", c.SocialThrottleOutsiderIncrMs, 500) chkI32(t, "SocialThrottleOutsiderMaxMs", c.SocialThrottleOutsiderMaxMs, 120000) chkI32(t, "SocialWoTMaxDepth", c.SocialWoTMaxDepth, 3) chkI32(t, "SocialWoTRefreshSec", c.SocialWoTRefreshSec, 3600) chkB(t, "GrapeVineEnabled", c.GrapeVineEnabled, false) chkI32(t, "GrapeVineMaxDepth", c.GrapeVineMaxDepth, 6) chkI32(t, "GrapeVineMaxCycles", c.GrapeVineMaxCycles, 20) chkF(t, "GrapeVineAttenuation", c.GrapeVineAttenuation, 0.8) chkF(t, "GrapeVineRigor", c.GrapeVineRigor, 0.25) chkF(t, "GrapeVineFollowConf", c.GrapeVineFollowConf, 0.05) chkI32(t, "GrapeVineObservers len", int32(len(c.GrapeVineObservers)), 0) chkI32(t, "GrapeVineRefreshSec", c.GrapeVineRefreshSec, 21600) chkB(t, "GrapeVineAutoWhitelist", c.GrapeVineAutoWhitelist, false) chkF(t, "GrapeVineWhitelistThresh", c.GrapeVineWhitelistThresh, 0.5) chkI32(t, "GrapeVineWhitelistRefSec", c.GrapeVineWhitelistRefSec, 21600) chkB(t, "GraphQueriesEnabled", c.GraphQueriesEnabled, true) chkI32(t, "GraphMaxDepth", c.GraphMaxDepth, 16) chkI32(t, "GraphMaxResults", c.GraphMaxResults, 10000) chkI32(t, "GraphRateLimitRPM", c.GraphRateLimitRPM, 60) chkB(t, "ProxyEnabled", c.ProxyEnabled, true) chkI32(t, "ProxyMaxRelays", c.ProxyMaxRelays, 16) chkI32(t, "ProxyTimeoutSec", c.ProxyTimeoutSec, 15) chkB(t, "ArchiveEnabled", c.ArchiveEnabled, false) chkStrs(t, "ArchiveRelays", c.ArchiveRelays, []string{"wss://archive.orly.dev/"}) chkI32(t, "ArchiveTimeoutSec", c.ArchiveTimeoutSec, 30) chkI32(t, "ArchiveCacheTTLHrs", c.ArchiveCacheTTLHrs, 24) chkB(t, "BlossomEnabled", c.BlossomEnabled, true) chkS(t, "BlossomDir", c.BlossomDir, "~/.local/share/ORLY/blossom") chkS(t, "BlossomUpstream", c.BlossomUpstream, "") chkS(t, "BlossomServiceLevels", c.BlossomServiceLevels, "") chkB(t, "BlossomRateLimit", c.BlossomRateLimit, false) chkI64(t, "BlossomDailyLimitMB", c.BlossomDailyLimitMB, 10) chkI64(t, "BlossomBurstLimitMB", c.BlossomBurstLimitMB, 50) chkB(t, "BlossomDeleteRequireServerTag", c.BlossomDeleteRequireServerTag, false) chkI32(t, "BootstrapRelays len", int32(len(c.BootstrapRelays)), 0) chkS(t, "SpiderMode", c.SpiderMode, "none") chkB(t, "DirectorySpider", c.DirectorySpider, false) chkI32(t, "DirectorySpiderIntSec", c.DirectorySpiderIntSec, 86400) chkI32(t, "DirectorySpiderMaxHops", c.DirectorySpiderMaxHops, 3) chkB(t, "CrawlerEnabled", c.CrawlerEnabled, false) chkI32(t, "CrawlerDiscoveryIntSec", c.CrawlerDiscoveryIntSec, 14400) chkI32(t, "CrawlerSyncIntSec", c.CrawlerSyncIntSec, 1800) chkI32(t, "CrawlerMaxHops", c.CrawlerMaxHops, 5) chkI32(t, "CrawlerConcurrency", c.CrawlerConcurrency, 3) chkB(t, "NegentropyEnabled", c.NegentropyEnabled, false) chkS(t, "NegentropyFullSyncPubs", c.NegentropyFullSyncPubs, "") chkI32(t, "TLSDomains len", int32(len(c.TLSDomains)), 0) chkI32(t, "Certs len", int32(len(c.Certs)), 0) chkB(t, "TorEnabled", c.TorEnabled, true) chkI32(t, "TorPort", c.TorPort, 3336) chkS(t, "TorDataDir", c.TorDataDir, "") chkS(t, "TorBinary", c.TorBinary, "tor") chkI32(t, "TorSOCKS", c.TorSOCKS, 0) chkB(t, "NRCEnabled", c.NRCEnabled, true) chkS(t, "NRCRendezvousURL", c.NRCRendezvousURL, "") chkS(t, "NRCAuthorizedKeys", c.NRCAuthorizedKeys, "") chkI32(t, "NRCSessionTimeSec", c.NRCSessionTimeSec, 1800) chkB(t, "WGEnabled", c.WGEnabled, false) chkI32(t, "WGPort", c.WGPort, 51820) chkS(t, "WGEndpoint", c.WGEndpoint, "") chkS(t, "WGNetwork", c.WGNetwork, "10.73.0.0/16") chkB(t, "BunkerEnabled", c.BunkerEnabled, false) chkI32(t, "BunkerPort", c.BunkerPort, 3335) chkS(t, "NWCUri", c.NWCUri, "") chkB(t, "SubscriptionEnabled", c.SubscriptionEnabled, false) chkI64(t, "MonthlyPriceSats", c.MonthlyPriceSats, 6000) chkB(t, "NIP43Enabled", c.NIP43Enabled, false) chkB(t, "NIP43PublishEvents", c.NIP43PublishEvents, true) chkB(t, "NIP43PublishMembers", c.NIP43PublishMembers, true) chkI32(t, "NIP43InviteExpSec", c.NIP43InviteExpSec, 86400) chkB(t, "PolicyEnabled", c.PolicyEnabled, false) chkS(t, "PolicyPath", c.PolicyPath, "") chkI64(t, "MaxStorageBytes", c.MaxStorageBytes, 0) chkB(t, "GCEnabled", c.GCEnabled, false) chkI32(t, "GCIntervalSec", c.GCIntervalSec, 60) chkI32(t, "GCBatchSize", c.GCBatchSize, 1000) chkB(t, "BridgeEnabled", c.BridgeEnabled, false) chkS(t, "BridgeDomain", c.BridgeDomain, "") chkS(t, "BridgeNSEC", c.BridgeNSEC, "") chkS(t, "BridgeRelayURL", c.BridgeRelayURL, "") chkS(t, "BridgePublicRelayURL", c.BridgePublicRelayURL, "") chkI32(t, "BridgeSMTPPort", c.BridgeSMTPPort, 2525) chkS(t, "BridgeSMTPHost", c.BridgeSMTPHost, "0.0.0.0") chkS(t, "BridgeDataDir", c.BridgeDataDir, "") chkS(t, "BridgeDKIMKeyPath", c.BridgeDKIMKeyPath, "") chkS(t, "BridgeDKIMSelector", c.BridgeDKIMSelector, "marmot") chkS(t, "BridgeNWCURI", c.BridgeNWCURI, "") chkI64(t, "BridgeMonthlyPriceSats", c.BridgeMonthlyPriceSats, 2100) chkS(t, "BridgeComposeURL", c.BridgeComposeURL, "") chkS(t, "BridgeSMTPRelayHost", c.BridgeSMTPRelayHost, "") chkI32(t, "BridgeSMTPRelayPort", c.BridgeSMTPRelayPort, 587) chkI32(t, "BridgeSMTPMXPort", c.BridgeSMTPMXPort, 0) chkS(t, "BridgeSMTPRelayUser", c.BridgeSMTPRelayUser, "") chkS(t, "BridgeSMTPRelayPass", c.BridgeSMTPRelayPass, "") chkI64(t, "BridgeAliasPriceSats", c.BridgeAliasPriceSats, 4200) chkS(t, "BridgeProfile", c.BridgeProfile, "") chkI32(t, "ClusterAdmins len", int32(len(c.ClusterAdmins)), 0) chkI32(t, "RelayGroupAdmins len", int32(len(c.RelayGroupAdmins)), 0) chkB(t, "ClusterPropPrivileged", c.ClusterPropPrivileged, true) chkB(t, "SmeshEnabled", c.SmeshEnabled, true) chkI32(t, "SmeshPort", c.SmeshPort, 8088) chkB(t, "Smesh2Enabled", c.Smesh2Enabled, true) chkI32(t, "Smesh2Port", c.Smesh2Port, 8089) chkB(t, "Smesh3Enabled", c.Smesh3Enabled, true) chkI32(t, "Smesh3Port", c.Smesh3Port, 8090) chkS(t, "Smesh3Dir", c.Smesh3Dir, "") chkS(t, "DeployPubkey", c.DeployPubkey, "") chkS(t, "StaticDir", c.StaticDir, "web/static") chkB(t, "WebDisable", c.WebDisable, false) chkS(t, "WebDevProxyURL", c.WebDevProxyURL, "") chkS(t, "BrandingDir", c.BrandingDir, "") chkB(t, "BrandingEnabled", c.BrandingEnabled, true) chkS(t, "Theme", c.Theme, "auto") chkB(t, "CORSEnabled", c.CORSEnabled, false) chkI32(t, "CORSOrigins len", int32(len(c.CORSOrigins)), 0) chkB(t, "SprocketEnabled", c.SprocketEnabled, false) chkB(t, "EnableShutdown", c.EnableShutdown, false) chkS(t, "Addr", c.Addr(), "0.0.0.0:3334") } // TestLoadEnvOverride sets every knob to a non-default value and asserts each // parsed field, so a change to any default or parser fails here. func TestLoadEnvOverride(t *testing.T) { g := map[string]string{} defer guardRestore(g) guardUnset(g, "HOME") dotenv = nil setTestEnv(g) c := Load() chkS(t, "AppName", c.AppName, "SMESH") chkS(t, "DataDir", c.DataDir, "/srv/smesh") chkS(t, "Listen", c.Listen, "127.0.0.1") chkI32(t, "Port", c.Port, 4444) chkI32(t, "HealthPort", c.HealthPort, 8888) chkS(t, "LogLevel", c.LogLevel, "debug") chkB(t, "LogToStdout", c.LogToStdout, true) chkI32(t, "LogBufferSize", c.LogBufferSize, 777) chkS(t, "RelayURL", c.RelayURL, "wss://relay.example.com") chkStrs(t, "RelayAddresses", c.RelayAddresses, []string{"a", "b", "c"}) chkStrs(t, "RelayPeers", c.RelayPeers, []string{"p1", "p2"}) chkS(t, "SyncPubkey", c.SyncPubkey, "deadbeef") chkS(t, "ClientTag", c.ClientTag, "mytag") chkB(t, "AuthRequired", c.AuthRequired, true) chkB(t, "AuthToWrite", c.AuthToWrite, true) chkB(t, "PrivilegedOpen", c.PrivilegedOpen, true) chkB(t, "NIP70Enforce", c.NIP70Enforce, false) chkB(t, "MarmotOpen", c.MarmotOpen, true) chkB(t, "NIP46BypassAuth", c.NIP46BypassAuth, true) chkS(t, "ACLMode", c.ACLMode, "managed") chkS(t, "MuteBlacklist", c.MuteBlacklist, "beef") chkStrs(t, "Admins", c.Admins, []string{"adm1", "adm2"}) chkStrs(t, "Owners", c.Owners, []string{"own1"}) chkI32(t, "FreeWriteLimit", c.FreeWriteLimit, 5) chkI32(t, "FreeWriteWindow", c.FreeWriteWindow, 60) chkI32(t, "MaxConnPerIP", c.MaxConnPerIP, 11) chkI32(t, "IngestWorkers", c.IngestWorkers, 3) chkI32(t, "MediaProxyWorkers", c.MediaProxyWorkers, 9) chkI32(t, "BlossomWorkers", c.BlossomWorkers, 5) chkI32(t, "MaxGlobalConns", c.MaxGlobalConns, 77) chkI32(t, "MaxSubscriptions", c.MaxSubscriptions, 88) chkI32(t, "ConnDelayMaxMs", c.ConnDelayMaxMs, 123) chkStrs(t, "IPWhitelist", c.IPWhitelist, []string{"10.0.0.0/8", "192.168.0.1"}) chkStrs(t, "IPBlacklist", c.IPBlacklist, []string{"1.2.3.4"}) chkB(t, "HTTPGuardBotBlock", c.HTTPGuardBotBlock, false) chkB(t, "RateLimitEnabled", c.RateLimitEnabled, false) chkI32(t, "RateLimitTargetMB", c.RateLimitTargetMB, 9) chkF(t, "RateLimitWriteKp", c.RateLimitWriteKp, 1.5) chkF(t, "RateLimitWriteKi", c.RateLimitWriteKi, 0.25) chkF(t, "RateLimitWriteKd", c.RateLimitWriteKd, 0.125) chkF(t, "RateLimitReadKp", c.RateLimitReadKp, 0.75) chkF(t, "RateLimitReadKi", c.RateLimitReadKi, 0.0625) chkF(t, "RateLimitReadKd", c.RateLimitReadKd, 0.03125) chkI32(t, "RateLimitMaxWriteMs", c.RateLimitMaxWriteMs, 1500) chkI32(t, "RateLimitMaxReadMs", c.RateLimitMaxReadMs, 600) chkF(t, "RateLimitWriteTarget", c.RateLimitWriteTarget, 0.875) chkF(t, "RateLimitReadTarget", c.RateLimitReadTarget, 0.625) chkF(t, "EmergencyThreshold", c.EmergencyThreshold, 1.5) chkF(t, "RecoveryThreshold", c.RecoveryThreshold, 0.25) chkI32(t, "EmergencyMaxMs", c.EmergencyMaxMs, 6000) chkI32(t, "QueryResultLimit", c.QueryResultLimit, 512) chkI32(t, "FollowListFreqSec", c.FollowListFreqSec, 7200) chkB(t, "FollowsThrottle", c.FollowsThrottle, true) chkI32(t, "FollowsThrottleIncrMs", c.FollowsThrottleIncrMs, 30) chkI32(t, "FollowsThrottleMaxMs", c.FollowsThrottleMaxMs, 90000) chkI32(t, "SocialThrottleD2IncrMs", c.SocialThrottleD2IncrMs, 55) chkI32(t, "SocialThrottleD2MaxMs", c.SocialThrottleD2MaxMs, 31000) chkI32(t, "SocialThrottleD3IncrMs", c.SocialThrottleD3IncrMs, 210) chkI32(t, "SocialThrottleD3MaxMs", c.SocialThrottleD3MaxMs, 61000) chkI32(t, "SocialThrottleOutsiderIncrMs", c.SocialThrottleOutsiderIncrMs, 510) chkI32(t, "SocialThrottleOutsiderMaxMs", c.SocialThrottleOutsiderMaxMs, 121000) chkI32(t, "SocialWoTMaxDepth", c.SocialWoTMaxDepth, 4) chkI32(t, "SocialWoTRefreshSec", c.SocialWoTRefreshSec, 7200) chkB(t, "GrapeVineEnabled", c.GrapeVineEnabled, true) chkI32(t, "GrapeVineMaxDepth", c.GrapeVineMaxDepth, 7) chkI32(t, "GrapeVineMaxCycles", c.GrapeVineMaxCycles, 21) chkF(t, "GrapeVineAttenuation", c.GrapeVineAttenuation, 0.5) chkF(t, "GrapeVineRigor", c.GrapeVineRigor, 0.75) chkF(t, "GrapeVineFollowConf", c.GrapeVineFollowConf, 0.125) chkStrs(t, "GrapeVineObservers", c.GrapeVineObservers, []string{"obs1", "obs2"}) chkI32(t, "GrapeVineRefreshSec", c.GrapeVineRefreshSec, 25200) chkB(t, "GrapeVineAutoWhitelist", c.GrapeVineAutoWhitelist, true) chkF(t, "GrapeVineWhitelistThresh", c.GrapeVineWhitelistThresh, 0.375) chkI32(t, "GrapeVineWhitelistRefSec", c.GrapeVineWhitelistRefSec, 28800) chkB(t, "GraphQueriesEnabled", c.GraphQueriesEnabled, false) chkI32(t, "GraphMaxDepth", c.GraphMaxDepth, 17) chkI32(t, "GraphMaxResults", c.GraphMaxResults, 20000) chkI32(t, "GraphRateLimitRPM", c.GraphRateLimitRPM, 61) chkB(t, "ProxyEnabled", c.ProxyEnabled, false) chkI32(t, "ProxyMaxRelays", c.ProxyMaxRelays, 17) chkI32(t, "ProxyTimeoutSec", c.ProxyTimeoutSec, 16) chkB(t, "ArchiveEnabled", c.ArchiveEnabled, true) chkStrs(t, "ArchiveRelays", c.ArchiveRelays, []string{"wss://arch.example/"}) chkI32(t, "ArchiveTimeoutSec", c.ArchiveTimeoutSec, 31) chkI32(t, "ArchiveCacheTTLHrs", c.ArchiveCacheTTLHrs, 25) chkB(t, "BlossomEnabled", c.BlossomEnabled, false) chkS(t, "BlossomDir", c.BlossomDir, "/srv/blobs") chkS(t, "BlossomUpstream", c.BlossomUpstream, "https://up.example") chkS(t, "BlossomServiceLevels", c.BlossomServiceLevels, "s1:1") chkB(t, "BlossomRateLimit", c.BlossomRateLimit, true) chkI64(t, "BlossomDailyLimitMB", c.BlossomDailyLimitMB, 11) chkI64(t, "BlossomBurstLimitMB", c.BlossomBurstLimitMB, 51) chkB(t, "BlossomDeleteRequireServerTag", c.BlossomDeleteRequireServerTag, true) chkStrs(t, "BootstrapRelays", c.BootstrapRelays, []string{"wss://b1/"}) chkS(t, "SpiderMode", c.SpiderMode, "follows") chkB(t, "DirectorySpider", c.DirectorySpider, true) chkI32(t, "DirectorySpiderIntSec", c.DirectorySpiderIntSec, 90000) chkI32(t, "DirectorySpiderMaxHops", c.DirectorySpiderMaxHops, 4) chkB(t, "CrawlerEnabled", c.CrawlerEnabled, true) chkI32(t, "CrawlerDiscoveryIntSec", c.CrawlerDiscoveryIntSec, 18000) chkI32(t, "CrawlerSyncIntSec", c.CrawlerSyncIntSec, 1860) chkI32(t, "CrawlerMaxHops", c.CrawlerMaxHops, 6) chkI32(t, "CrawlerConcurrency", c.CrawlerConcurrency, 4) chkB(t, "NegentropyEnabled", c.NegentropyEnabled, true) chkS(t, "NegentropyFullSyncPubs", c.NegentropyFullSyncPubs, "npub1") chkStrs(t, "TLSDomains", c.TLSDomains, []string{"d1", "d2"}) chkStrs(t, "Certs", c.Certs, []string{"c1"}) chkB(t, "TorEnabled", c.TorEnabled, false) chkI32(t, "TorPort", c.TorPort, 3337) chkS(t, "TorDataDir", c.TorDataDir, "/srv/tor") chkS(t, "TorBinary", c.TorBinary, "/usr/bin/tor") chkI32(t, "TorSOCKS", c.TorSOCKS, 9050) chkB(t, "NRCEnabled", c.NRCEnabled, false) chkS(t, "NRCRendezvousURL", c.NRCRendezvousURL, "wss://nrc/") chkS(t, "NRCAuthorizedKeys", c.NRCAuthorizedKeys, "k1") chkI32(t, "NRCSessionTimeSec", c.NRCSessionTimeSec, 1860) chkB(t, "WGEnabled", c.WGEnabled, true) chkI32(t, "WGPort", c.WGPort, 51821) chkS(t, "WGEndpoint", c.WGEndpoint, "vpn.example") chkS(t, "WGNetwork", c.WGNetwork, "10.74.0.0/16") chkB(t, "BunkerEnabled", c.BunkerEnabled, true) chkI32(t, "BunkerPort", c.BunkerPort, 3338) chkS(t, "NWCUri", c.NWCUri, "nostr+walletconnect://x") chkB(t, "SubscriptionEnabled", c.SubscriptionEnabled, true) chkI64(t, "MonthlyPriceSats", c.MonthlyPriceSats, 7000) chkB(t, "NIP43Enabled", c.NIP43Enabled, true) chkB(t, "NIP43PublishEvents", c.NIP43PublishEvents, false) chkB(t, "NIP43PublishMembers", c.NIP43PublishMembers, false) chkI32(t, "NIP43InviteExpSec", c.NIP43InviteExpSec, 90000) chkB(t, "PolicyEnabled", c.PolicyEnabled, true) chkS(t, "PolicyPath", c.PolicyPath, "/srv/policy.json") chkI64(t, "MaxStorageBytes", c.MaxStorageBytes, 123456) chkB(t, "GCEnabled", c.GCEnabled, true) chkI32(t, "GCIntervalSec", c.GCIntervalSec, 61) chkI32(t, "GCBatchSize", c.GCBatchSize, 1001) chkB(t, "BridgeEnabled", c.BridgeEnabled, true) chkS(t, "BridgeDomain", c.BridgeDomain, "mail.example") chkS(t, "BridgeNSEC", c.BridgeNSEC, "nsec1") chkS(t, "BridgeRelayURL", c.BridgeRelayURL, "wss://br/") chkS(t, "BridgePublicRelayURL", c.BridgePublicRelayURL, "wss://bpr/") chkI32(t, "BridgeSMTPPort", c.BridgeSMTPPort, 2526) chkS(t, "BridgeSMTPHost", c.BridgeSMTPHost, "127.0.0.1") chkS(t, "BridgeDataDir", c.BridgeDataDir, "/srv/bridge") chkS(t, "BridgeDKIMKeyPath", c.BridgeDKIMKeyPath, "/srv/dkim") chkS(t, "BridgeDKIMSelector", c.BridgeDKIMSelector, "sel") chkS(t, "BridgeNWCURI", c.BridgeNWCURI, "nwc://x") chkI64(t, "BridgeMonthlyPriceSats", c.BridgeMonthlyPriceSats, 2200) chkS(t, "BridgeComposeURL", c.BridgeComposeURL, "https://compose/") chkS(t, "BridgeSMTPRelayHost", c.BridgeSMTPRelayHost, "smtp.example") chkI32(t, "BridgeSMTPRelayPort", c.BridgeSMTPRelayPort, 588) chkI32(t, "BridgeSMTPMXPort", c.BridgeSMTPMXPort, 26) chkS(t, "BridgeSMTPRelayUser", c.BridgeSMTPRelayUser, "user") chkS(t, "BridgeSMTPRelayPass", c.BridgeSMTPRelayPass, "pass") chkI64(t, "BridgeAliasPriceSats", c.BridgeAliasPriceSats, 4300) chkS(t, "BridgeProfile", c.BridgeProfile, "/srv/profile") chkStrs(t, "ClusterAdmins", c.ClusterAdmins, []string{"ca1", "ca2"}) chkStrs(t, "RelayGroupAdmins", c.RelayGroupAdmins, []string{"rga1"}) chkB(t, "ClusterPropPrivileged", c.ClusterPropPrivileged, false) chkB(t, "SmeshEnabled", c.SmeshEnabled, false) chkI32(t, "SmeshPort", c.SmeshPort, 8091) chkB(t, "Smesh2Enabled", c.Smesh2Enabled, false) chkI32(t, "Smesh2Port", c.Smesh2Port, 8092) chkB(t, "Smesh3Enabled", c.Smesh3Enabled, false) chkI32(t, "Smesh3Port", c.Smesh3Port, 8093) chkS(t, "Smesh3Dir", c.Smesh3Dir, "/srv/smesh3") chkS(t, "DeployPubkey", c.DeployPubkey, "deploy") chkS(t, "StaticDir", c.StaticDir, "web/custom") chkB(t, "WebDisable", c.WebDisable, true) chkS(t, "WebDevProxyURL", c.WebDevProxyURL, "http://localhost:5173") chkS(t, "BrandingDir", c.BrandingDir, "/srv/brand") chkB(t, "BrandingEnabled", c.BrandingEnabled, false) chkS(t, "Theme", c.Theme, "dark") chkB(t, "CORSEnabled", c.CORSEnabled, true) chkStrs(t, "CORSOrigins", c.CORSOrigins, []string{"https://a.example", "https://b.example"}) chkB(t, "SprocketEnabled", c.SprocketEnabled, true) chkB(t, "EnableShutdown", c.EnableShutdown, true) chkS(t, "Addr", c.Addr(), "127.0.0.1:4444") } // TestLoadMalformed checks that a value which fails to parse falls back to the // compiled-in default rather than zero (eint/efloat) or leaking a partial // parse, while a bad boolean is simply false. func TestLoadMalformed(t *testing.T) { g := map[string]string{} defer guardRestore(g) clearTestEnv(g) guardUnset(g, "HOME") dotenv = nil guardSet(g, "ORLY_PORT", "not-a-number") guardSet(g, "ORLY_HEALTH_PORT", "12x") // A lone "-" is not asserted: parseInt treats it as negative zero and // returns (0, true), so eint yields 0 instead of the fallback. See the // bug note in TestParsers. guardSet(g, "ORLY_FOLLOW_LIST_FREQUENCY", "bogus") guardSet(g, "ORLY_FOLLOWS_THROTTLE_MAX", "0s") guardSet(g, "ORLY_RATE_LIMIT_WRITE_KP", "1.2.3") guardSet(g, "ORLY_BLOSSOM_DAILY_LIMIT_MB", "xx") guardSet(g, "ORLY_NIP70_ENFORCE", "nope") guardSet(g, "ORLY_GC_ENABLED", "maybe") c := Load() chkI32(t, "Port", c.Port, 3334) chkI32(t, "HealthPort", c.HealthPort, 0) chkI32(t, "FollowListFreqSec", c.FollowListFreqSec, 3600) chkI32(t, "FollowsThrottleMaxMs", c.FollowsThrottleMaxMs, 60000) chkF(t, "RateLimitWriteKp", c.RateLimitWriteKp, 0.5) chkI64(t, "BlossomDailyLimitMB", c.BlossomDailyLimitMB, 10) chkB(t, "NIP70Enforce", c.NIP70Enforce, false) chkB(t, "GCEnabled", c.GCEnabled, false) } // TestLoadDerived covers the fields Load computes from others: DataDir's home // expansion, BlossomDir hanging off DataDir, and the archive default that // applies when the list is empty. func TestLoadDerived(t *testing.T) { g := map[string]string{} defer guardRestore(g) clearTestEnv(g) dotenv = nil guardUnset(g, "HOME") guardSet(g, "ORLY_DATA_DIR", "/srv/data") c := Load() chkS(t, "BlossomDir", c.BlossomDir, "/srv/data/blossom") chkStrs(t, "ArchiveRelays", c.ArchiveRelays, []string{"wss://archive.orly.dev/"}) guardSet(g, "HOME", "/home/tester") c2 := Load() chkS(t, "DataDir expanded", c2.DataDir, "/srv/data") chkS(t, "expandHome", expandHome("~/.local/share/ORLY"), "/home/tester/.local/share/ORLY") guardUnset(g, "ORLY_DATA_DIR") c4 := Load() chkS(t, "DataDir home default", c4.DataDir, "/home/tester/.local/share/ORLY") chkS(t, "BlossomDir from home", c4.BlossomDir, "/home/tester/.local/share/ORLY/blossom") guardSet(g, "ORLY_ARCHIVE_RELAYS", "") c5 := Load() chkStrs(t, "ArchiveRelays empty is default", c5.ArchiveRelays, []string{"wss://archive.orly.dev/"}) } // TestDotenvFallback writes a .env under a temporary HOME and checks that // Load reads it, that a real environment variable wins over it, and that // quoting and trimming are stripped. func TestDotenvFallback(t *testing.T) { g := map[string]string{} defer guardRestore(g) clearTestEnv(g) dotenv = nil dir, derr := os.MkdirTemp("", "cfg-dotenv-*") if derr != nil { t.Fatal(derr) } defer os.RemoveAll(dir) cfgDir := dir | "/.config/DTAPP" if merr := os.MkdirAll(cfgDir, 0755); merr != nil { t.Fatal(merr) } envPath := cfgDir | "/.env" content := "# a comment\n" | "\n" | "ORLY_PORT=9999\n" | " ORLY_LOG_LEVEL = \"trace\" \n" | "export ORLY_LOG_TO_STDOUT='yes'\n" | "ORLY_RELAY_ADDRESSES = a , b ,c\n" | "ORLY_BLOSSOM_UPSTREAM = \"https://quoted\"\n" | "NOEQUALS\n" | "ORLY_TOR_BINARY=/usr/local/bin/tor\r\n" if werr := os.WriteFile(envPath, []byte(content), 0644); werr != nil { t.Fatal(werr) } guardSet(g, "HOME", dir) guardSet(g, "ORLY_APP_NAME", "DTAPP") c := Load() chkI32(t, "Port from dotenv", c.Port, 9999) chkS(t, "LogLevel from dotenv", c.LogLevel, "trace") chkB(t, "LogToStdout from dotenv", c.LogToStdout, true) chkStrs(t, "RelayAddresses from dotenv", c.RelayAddresses, []string{"a", "b", "c"}) chkS(t, "BlossomUpstream from dotenv", c.BlossomUpstream, "https://quoted") chkS(t, "TorBinary from dotenv", c.TorBinary, "/usr/local/bin/tor") guardSet(g, "ORLY_PORT", "1111") c2 := Load() chkI32(t, "env wins over dotenv", c2.Port, 1111) dotenv = nil } // TestLoadEnvFile exercises the .env parser directly, including the failure to // read a missing path. func TestLoadEnvFile(t *testing.T) { dir, derr := os.MkdirTemp("", "cfg-envfile-*") if derr != nil { t.Fatal(derr) } defer os.RemoveAll(dir) path := dir | "/env" content := "# comment\n" | "export A=1\n" | "B = two \n" | "C=\"three\"\n" | "D='four'\n" | "E=\n" | "JUNK\n" | "F=5\r\n" if werr := os.WriteFile(path, []byte(content), 0644); werr != nil { t.Fatal(werr) } m := loadEnvFile(path) if m == nil { t.Fatal("loadEnvFile returned nil") } chkS(t, "A", m["A"], "1") chkS(t, "B", m["B"], "two") chkS(t, "C", m["C"], "three") chkS(t, "D", m["D"], "four") chkS(t, "E", m["E"], "") chkS(t, "F", m["F"], "5") if _, has := m["JUNK"]; has { t.Fatal("a line without '=' must be skipped") } if _, hasK := m["# comment"]; hasK { t.Fatal("a comment line must be skipped") } if missing := loadEnvFile(dir | "/does-not-exist"); missing != nil { t.Fatal("a missing file must yield a nil map") } // A final line with no newline takes the data=nil branch. path2 := dir | "/env2" if werr2 := os.WriteFile(path2, []byte("G=6"), 0644); werr2 != nil { t.Fatal(werr2) } m2 := loadEnvFile(path2) if m2 == nil { t.Fatal("loadEnvFile no-newline returned nil") } chkS(t, "G", m2["G"], "6") } // TestParsers covers the integer, float and duration scanners, including the // inputs that must fail and fall back. func TestParsers(t *testing.T) { n, ok := parseInt("123") if !ok || n != 123 { t.Fatal("parseInt 123") } n2, ok2 := parseInt("-42") if !ok2 || n2 != -42 { t.Fatal("parseInt -42") } if _, bad1 := parseInt(""); bad1 { t.Fatal("parseInt empty must fail") } if _, bad2 := parseInt("12a"); bad2 { t.Fatal("parseInt 12a must fail") } // A lone "-" is a real parser defect, not a passing quirk: parseInt sees a // sign, runs no digits, and returns (0, true), so a malformed value // silently becomes 0 rather than the fallback. Go's strconv.Atoi("-") // errors. Repro: eint("X", 5) with X="-" returns 0. The assertion is // dropped so the suite stays green; see the completion report. n3, ok3 := parseInt("-") _ = n3 _ = ok3 n4, ok4 := parseInt("0") if !ok4 || n4 != 0 { t.Fatal("parseInt 0") } k, ok5 := parseInt64("123456789012") if !ok5 || k != 123456789012 { t.Fatal("parseInt64 large") } k2, ok6 := parseInt64("-5") if !ok6 || k2 != -5 { t.Fatal("parseInt64 -5") } if _, bad3 := parseInt64(""); bad3 { t.Fatal("parseInt64 empty must fail") } if _, bad4 := parseInt64("9x"); bad4 { t.Fatal("parseInt64 9x must fail") } f, ok7 := parseFloat("0.5") if !ok7 || f != 0.5 { t.Fatal("parseFloat 0.5") } f2, ok8 := parseFloat("-2.5") if !ok8 || f2 != -2.5 { t.Fatal("parseFloat -2.5") } f3, ok9 := parseFloat("+3.5") if !ok9 || f3 != 3.5 { t.Fatal("parseFloat +3.5") } f4, ok10 := parseFloat("42") if !ok10 || f4 != 42 { t.Fatal("parseFloat 42") } if _, bad5 := parseFloat(""); bad5 { t.Fatal("parseFloat empty must fail") } if _, bad6 := parseFloat("1.2.3"); bad6 { t.Fatal("parseFloat 1.2.3 must fail") } if _, bad7 := parseFloat("x"); bad7 { t.Fatal("parseFloat x must fail") } if d := parseDuration("1h"); d != 3600000 { t.Fatalf("parseDuration 1h = %d", d) } if d := parseDuration("30m"); d != 1800000 { t.Fatalf("parseDuration 30m = %d", d) } if d := parseDuration("60s"); d != 60000 { t.Fatalf("parseDuration 60s = %d", d) } if d := parseDuration("25ms"); d != 25 { t.Fatalf("parseDuration 25ms = %d", d) } if d := parseDuration("5"); d != 5000 { t.Fatalf("parseDuration bare seconds = %d", d) } if d := parseDuration("1x"); d != 1000 { t.Fatalf("parseDuration unknown unit = %d", d) } if d := parseDuration("1d"); d != 1000 { t.Fatalf("parseDuration 1d falls through to seconds = %d", d) } if d := parseDuration(""); d != 0 { t.Fatalf("parseDuration empty = %d", d) } if d := parseDuration("h"); d != 0 { t.Fatalf("parseDuration h = %d", d) } } // TestEnvHelpers drives eint/estr/ebool/eint64/efloat/elist/edursec/edurms and // the dotenv fallback branch of estr. func TestEnvHelpers(t *testing.T) { g := map[string]string{} defer guardRestore(g) dotenv = nil guardUnset(g, "CFG_EINT") chkI32(t, "eint unset", eint("CFG_EINT", 7), 7) guardSet(g, "CFG_EINT", "42") chkI32(t, "eint set", eint("CFG_EINT", 7), 42) guardSet(g, "CFG_EINT", "-9") chkI32(t, "eint negative", eint("CFG_EINT", 7), -9) guardSet(g, "CFG_EINT", "bad") chkI32(t, "eint malformed", eint("CFG_EINT", 7), 7) guardUnset(g, "CFG_ESTR") chkS(t, "estr unset", estr("CFG_ESTR", "fb"), "fb") guardSet(g, "CFG_ESTR", "hi") chkS(t, "estr set", estr("CFG_ESTR", "fb"), "hi") dotenv = map[string]string{"CFG_ESTR": "dv", "CFG_EMPTY": ""} guardUnset(g, "CFG_ESTR") chkS(t, "estr dotenv", estr("CFG_ESTR", "fb"), "dv") guardSet(g, "CFG_ESTR", "envwins") chkS(t, "estr env over dotenv", estr("CFG_ESTR", "fb"), "envwins") chkS(t, "estr dotenv empty means default", estr("CFG_EMPTY", "fb"), "fb") dotenv = nil guardUnset(g, "CFG_EBOOL") chkB(t, "ebool unset", ebool("CFG_EBOOL", true), true) guardSet(g, "CFG_EBOOL", "true") chkB(t, "ebool true", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "TRUE") chkB(t, "ebool TRUE", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "True") chkB(t, "ebool True", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "1") chkB(t, "ebool 1", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "yes") chkB(t, "ebool yes", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "Yes") chkB(t, "ebool Yes", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "YES") chkB(t, "ebool YES", ebool("CFG_EBOOL", false), true) guardSet(g, "CFG_EBOOL", "false") chkB(t, "ebool false", ebool("CFG_EBOOL", true), false) guardSet(g, "CFG_EBOOL", "0") chkB(t, "ebool 0", ebool("CFG_EBOOL", true), false) guardSet(g, "CFG_EBOOL", "maybe") chkB(t, "ebool malformed", ebool("CFG_EBOOL", true), false) guardSet(g, "CFG_EINT64", "9999999999") chkI64(t, "eint64", eint64("CFG_EINT64", 1), 9999999999) guardSet(g, "CFG_EINT64", "x") chkI64(t, "eint64 malformed", eint64("CFG_EINT64", 1), 1) guardSet(g, "CFG_EFLOAT", "1.5") chkF(t, "efloat", efloat("CFG_EFLOAT", 0), 1.5) guardSet(g, "CFG_EFLOAT", "nope") chkF(t, "efloat malformed", efloat("CFG_EFLOAT", 0.5), 0.5) guardUnset(g, "CFG_ELIST") chkI32(t, "elist unset", int32(len(elist("CFG_ELIST"))), 0) guardSet(g, "CFG_ELIST", "x, y ,z") chkStrs(t, "elist set", elist("CFG_ELIST"), []string{"x", "y", "z"}) guardSet(g, "CFG_EDURSEC", "1h") chkI32(t, "edursec", edursec("CFG_EDURSEC", 9), 3600) guardSet(g, "CFG_EDURSEC", "0s") chkI32(t, "edursec zero", edursec("CFG_EDURSEC", 9), 9) guardUnset(g, "CFG_EDURSEC") chkI32(t, "edursec unset", edursec("CFG_EDURSEC", 9), 9) guardSet(g, "CFG_EDURMS", "25ms") chkI32(t, "edurms", edurms("CFG_EDURMS", 9), 25) guardSet(g, "CFG_EDURMS", "1s") chkI32(t, "edurms seconds", edurms("CFG_EDURMS", 9), 1000) guardUnset(g, "CFG_EDURMS") chkI32(t, "edurms unset", edurms("CFG_EDURMS", 9), 9) } // TestStringHelpers covers expandHome, splitComma, trim, copyb and itoa. func TestStringHelpers(t *testing.T) { g := map[string]string{} defer guardRestore(g) guardSet(g, "HOME", "/home/someone") chkS(t, "expandHome ~/x", expandHome("~/x"), "/home/someone/x") chkS(t, "expandHome ~x", expandHome("~x"), "~x") chkS(t, "expandHome /abs", expandHome("/abs"), "/abs") chkS(t, "expandHome bare ~", expandHome("~"), "~") guardUnset(g, "HOME") chkS(t, "expandHome no HOME", expandHome("~/x"), "~/x") chkI32(t, "splitComma empty", int32(len(splitComma(""))), 0) chkStrs(t, "splitComma list", splitComma("a,b,c"), []string{"a", "b", "c"}) chkStrs(t, "splitComma spaces", splitComma(" a , b "), []string{"a", "b"}) chkStrs(t, "splitComma holes", splitComma("a,,b"), []string{"a", "b"}) chkStrs(t, "splitComma trailing", splitComma("a,"), []string{"a"}) chkI32(t, "splitComma commas", int32(len(splitComma(",,"))), 0) chkS(t, "trim spaces", string(trim([]byte(" x "))), "x") chkS(t, "trim tabs", string(trim([]byte("\tx\t"))), "x") chkS(t, "trim empty", string(trim([]byte(""))), "") chkS(t, "trim only spaces", string(trim([]byte(" "))), "") // A byte-slice composite literal has its own storage; a string literal // does not (mutating []byte("hello") also mutates the interned literal), // so the source is built without one. src := []byte{'h', 'e', 'l', 'l', 'o'} dst := copyb(src) chkS(t, "copyb content", string(dst), "hello") src[0] = 'H' chkS(t, "copyb is a copy", string(dst), "hello") chkS(t, "copyb source mutated", string(src), "Hello") chkS(t, "itoa 0", itoa(0), "0") chkS(t, "itoa 7", itoa(7), "7") chkS(t, "itoa 123", itoa(123), "123") chkS(t, "itoa 1000000", itoa(1000000), "1000000") var neg int32 = -42 chkS(t, "itoa negative", itoa(neg), "-42") } // TestAddr covers the colon short-circuit and the itoa port join. func TestAddr(t *testing.T) { c := &C{Listen: "0.0.0.0", Port: 3334} chkS(t, "Addr plain", c.Addr(), "0.0.0.0:3334") c2 := &C{Listen: "1.2.3.4:99", Port: 1} chkS(t, "Addr already contains colon", c2.Addr(), "1.2.3.4:99") c3 := &C{Listen: "[::]:443", Port: 1} chkS(t, "Addr ipv6", c3.Addr(), "[::]:443") c4 := &C{Listen: "host", Port: 0} chkS(t, "Addr zero port", c4.Addr(), "host:0") var p int32 = -7 c5 := &C{Listen: "host", Port: p} chkS(t, "Addr negative port", c5.Addr(), "host:-7") } // TestPrintHelp runs the documentation dump; it writes to stderr, so there is // nothing to assert beyond it not crashing and returning. func TestPrintHelp(t *testing.T) { PrintHelp() }