package distworker import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestHTTPConfigFromEnv_Defaults verifies the documented defaults when no // HTTP-related env vars are set: 0.0.0.0:27401 and empty public URL / // login / password. The default port was bumped from 7401 to 27401 to avoid // colliding with the main RSMon app when the worker is co-located. func TestHTTPConfigFromEnv_Defaults(t *testing.T) { t.Setenv("WORKER_HOST", "") t.Setenv("WORKER_PORT", "") t.Setenv("PUBLIC_URL", "") t.Setenv("WORKER_URL", "") t.Setenv("WORKER_LOGIN", "") t.Setenv("WORKER_PASSWORD", "") cfg := HTTPConfigFromEnv() assert.Equal(t, DefaultHTTPHost, cfg.Host, "host should default to 0.0.0.0") assert.Equal(t, DefaultHTTPPort, cfg.Port, "port should default to 27401") assert.Equal(t, "", cfg.PublicURL) assert.Equal(t, "", cfg.Login) assert.Equal(t, "", cfg.Password) assert.False(t, cfg.IsAuthConfigured()) } // TestHTTPConfigFromEnv_Overrides verifies the env vars win over the // defaults when explicitly set, including a non-default port. func TestHTTPConfigFromEnv_Overrides(t *testing.T) { t.Setenv("WORKER_HOST", "127.0.0.1") t.Setenv("WORKER_PORT", "9100") t.Setenv("PUBLIC_URL", "https://worker.example.com") t.Setenv("WORKER_LOGIN", "ops") t.Setenv("WORKER_PASSWORD", "s3cret") cfg := HTTPConfigFromEnv() assert.Equal(t, "127.0.0.1", cfg.Host) assert.Equal(t, 9100, cfg.Port) assert.Equal(t, "https://worker.example.com", cfg.PublicURL) assert.Equal(t, "ops", cfg.Login) assert.Equal(t, "s3cret", cfg.Password) assert.True(t, cfg.IsAuthConfigured()) } // TestHTTPConfigFromEnv_PortGarbageFallsBackToDefault covers the "operator // fat-fingered WORKER_PORT" case: the helper must not panic and must // fall back to DefaultHTTPPort instead of starting on a zero port. func TestHTTPConfigFromEnv_PortGarbageFallsBackToDefault(t *testing.T) { t.Setenv("WORKER_PORT", "not-a-port") cfg := HTTPConfigFromEnv() assert.Equal(t, DefaultHTTPPort, cfg.Port, "unparseable WORKER_PORT should fall back to the default") } // TestHTTPConfigFromEnv_PortOutOfRangeFallsBackToDefault covers zero and // out-of-range port values which are rejected by url/strconv semantics. func TestHTTPConfigFromEnv_PortOutOfRangeFallsBackToDefault(t *testing.T) { for _, v := range []string{"0", "-1", "70000"} { t.Run("port="+v, func(t *testing.T) { t.Setenv("WORKER_PORT", v) cfg := HTTPConfigFromEnv() assert.Equal(t, DefaultHTTPPort, cfg.Port, "WORKER_PORT=%q should fall back to the default", v) }) } } // TestPublicURLFromEnv_PublicURLWins pins the canonical-source rule: // when both PUBLIC_URL and the legacy WORKER_URL are set, PUBLIC_URL wins // and the source is reported as canonical. func TestPublicURLFromEnv_PublicURLWins(t *testing.T) { t.Setenv("PUBLIC_URL", "https://canonical.example.com") t.Setenv("WORKER_URL", "http://legacy.example.com:27401") url, source := PublicURLFromEnv() assert.Equal(t, "https://canonical.example.com", url) assert.Equal(t, PublicURLSourceCanonical, source) cfg := HTTPConfigFromEnv() assert.Equal(t, "https://canonical.example.com", cfg.PublicURL) } // TestPublicURLFromEnv_LegacyFallback verifies the bounded migration: the // legacy WORKER_URL is resolved when PUBLIC_URL is unset and the source is // reported as legacy so the caller can emit the deprecation warning. func TestPublicURLFromEnv_LegacyFallback(t *testing.T) { t.Setenv("PUBLIC_URL", "") t.Setenv("WORKER_URL", "https://legacy.example.com") url, source := PublicURLFromEnv() assert.Equal(t, "https://legacy.example.com", url) assert.Equal(t, PublicURLSourceLegacy, source) } // TestPublicURLFromEnv_None verifies that no configured origin reports the // none source and an empty value. func TestPublicURLFromEnv_None(t *testing.T) { t.Setenv("PUBLIC_URL", "") t.Setenv("WORKER_URL", "") url, source := PublicURLFromEnv() assert.Equal(t, "", url) assert.Equal(t, PublicURLSourceNone, source) } // TestValidateHTTPConfig_LoginXORPasswordRejected covers the central // invariant: a mixed login/password state is a config bug and must // fail fast. func TestValidateHTTPConfig_LoginXORPasswordRejected(t *testing.T) { t.Run("only login set", func(t *testing.T) { err := ValidateHTTPConfig(HTTPConfig{ Host: "0.0.0.0", Port: 27401, Login: "ops", Password: "", }, false) require.Error(t, err) assert.Contains(t, err.Error(), "WORKER_LOGIN and WORKER_PASSWORD") }) t.Run("only password set", func(t *testing.T) { err := ValidateHTTPConfig(HTTPConfig{ Host: "0.0.0.0", Port: 27401, Login: "", Password: "s3cret", }, false) require.Error(t, err) assert.Contains(t, err.Error(), "WORKER_LOGIN and WORKER_PASSWORD") }) } // TestValidateHTTPConfig_BothEmptyAllowedWhenNotListening reflects the // Task 2 deferral: while the HTTP listener is not started (Task 3), // leaving both unset is permitted so a worker that does not yet need // the web app can still start. func TestValidateHTTPConfig_BothEmptyAllowedWhenNotListening(t *testing.T) { err := ValidateHTTPConfig(HTTPConfig{ Host: "0.0.0.0", Port: 27401, Login: "", Password: "", }, false) assert.NoError(t, err) } // TestValidateHTTPConfig_BothEmptyRejectedWhenListening confirms that // when Task 3 actually starts the listener, an unauthenticated listener // is refused. func TestValidateHTTPConfig_BothEmptyRejectedWhenListening(t *testing.T) { err := ValidateHTTPConfig(HTTPConfig{ Host: "0.0.0.0", Port: 27401, Login: "", Password: "", }, true) require.Error(t, err) assert.Contains(t, err.Error(), "refused to start") } // TestValidateHTTPConfig_URLAbsoluteRequired covers the relative-URL // rejection: peer workers and the main app need a concrete origin. func TestValidateHTTPConfig_URLAbsoluteRequired(t *testing.T) { cases := []struct { name string url string }{ {"relative path", "/worker"}, {"missing scheme", "example.com"}, {"missing host", "https://"}, {"empty after trim", ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cfg := HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: tc.url} err := ValidateHTTPConfig(cfg, false) // empty url is allowed if tc.url == "" { assert.NoError(t, err) return } require.Error(t, err) }) } } // TestValidateHTTPConfig_URLSchemeAllowed covers the two accepted schemes. func TestValidateHTTPConfig_URLSchemeAllowed(t *testing.T) { for _, scheme := range []string{"http", "https"} { t.Run(scheme, func(t *testing.T) { cfg := HTTPConfig{ Host: "0.0.0.0", Port: 27401, PublicURL: scheme + "://localhost:27401", } assert.NoError(t, ValidateHTTPConfig(cfg, false)) }) } } // TestValidateHTTPConfig_URLSchemeRejected covers non-http(s) schemes. func TestValidateHTTPConfig_URLSchemeRejected(t *testing.T) { for _, scheme := range []string{"ftp", "file", "ws", "wss"} { t.Run(scheme, func(t *testing.T) { cfg := HTTPConfig{ Host: "0.0.0.0", Port: 27401, PublicURL: scheme + "://localhost:27401", } err := ValidateHTTPConfig(cfg, false) require.Error(t, err) assert.Contains(t, strings.ToLower(err.Error()), "scheme must be http or https") }) } } // TestValidatePublicURL_OriginShapeRejected covers the plan's negative // cases: the advertised origin must be scheme + authority only, with no // userinfo, query, fragment, or ambiguous path. func TestValidatePublicURL_OriginShapeRejected(t *testing.T) { cases := []struct { name string url string want string }{ {"userinfo", "https://user:pass@worker.example.com", "userinfo"}, {"query", "https://worker.example.com?x=1", "query"}, {"fragment", "https://worker.example.com#frag", "fragment"}, {"path", "https://worker.example.com/web", "path"}, {"path nested", "https://worker.example.com/raft/", "path"}, {"missing scheme", "worker.example.com", "absolute url"}, {"missing host", "https://", "absolute url"}, {"bad scheme", "ftp://worker.example.com", "scheme must be http or https"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := ValidatePublicURL(tc.url) require.Error(t, err) assert.Contains(t, strings.ToLower(err.Error()), tc.want) assert.Contains(t, err.Error(), EnvPublicURL, "error must name the canonical PUBLIC_URL variable") }) } } // TestValidatePublicURL_RootSlashAllowed confirms that an empty path and // the root path "/" are both accepted as an origin. func TestValidatePublicURL_RootSlashAllowed(t *testing.T) { for _, u := range []string{"https://worker.example.com", "https://worker.example.com/"} { assert.NoError(t, ValidatePublicURL(u)) } } // TestValidatePublicURL_HostnameRequired pins the hostname rule: an // authority that parses to no hostname (for example "https://:27401") // must be rejected even though it has a host:port string. func TestValidatePublicURL_HostnameRequired(t *testing.T) { for _, u := range []string{"https://:27401", "http://:7401"} { t.Run(u, func(t *testing.T) { err := ValidatePublicURL(u) require.Error(t, err) assert.Contains(t, strings.ToLower(err.Error()), "must include a host") }) } } // TestValidateAdvertisedURL_Tolerant pins the legacy-tolerant shape used // for WORKER_URL and control-plane-supplied endpoints: an absolute // http(s) URL with a scheme, host, and hostname. Paths, userinfo, query, // and fragments that previously ran must keep validating; only values // that could never be dialed (relative, bad scheme, missing hostname) // are rejected. func TestValidateAdvertisedURL_Tolerant(t *testing.T) { for _, u := range []string{ "https://worker.example.com", "https://worker.example.com/web", "https://worker.example.com/raft/", "http://localhost:27401", "https://user:pass@worker.example.com/web?x=1#frag", } { assert.NoError(t, ValidateAdvertisedURL(u), "tolerant validator must accept %q", u) } for _, u := range []string{ "worker.example.com", "https://", "https://:27401", "ftp://worker.example.com", "file:///tmp/x", } { assert.Error(t, ValidateAdvertisedURL(u), "tolerant validator must reject %q", u) } } // TestValidateHTTPConfig_LegacySourceTolerant is the core bounded-migration // guarantee: a legacy WORKER_URL that previously ran (path, userinfo, even // plain HTTP in production) must not newly fail startup. Only genuinely // unusable values are rejected. func TestValidateHTTPConfig_LegacySourceTolerant(t *testing.T) { t.Setenv("DEPLOY_ENV", "production") for _, u := range []string{ "https://worker.example.com", "https://worker.example.com/web", "http://worker.example.com", } { cfg := HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: u, URLSource: PublicURLSourceLegacy} assert.NoError(t, ValidateHTTPConfig(cfg, false), "legacy WORKER_URL=%q must not newly fail startup", u) } cfg := HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: "https://:27401", URLSource: PublicURLSourceLegacy} err := ValidateHTTPConfig(cfg, false) require.Error(t, err) assert.Contains(t, err.Error(), EnvWorkerURLLegacy, "legacy diagnostics must name the WORKER_URL source") } // TestValidateHTTPConfig_CanonicalSourceStrict verifies the canonical // PUBLIC_URL stays strict even when the same value would be tolerated as // a legacy WORKER_URL. func TestValidateHTTPConfig_CanonicalSourceStrict(t *testing.T) { cfg := HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: "https://worker.example.com/web", URLSource: PublicURLSourceCanonical} err := ValidateHTTPConfig(cfg, false) require.Error(t, err) assert.Contains(t, err.Error(), EnvPublicURL, "canonical diagnostics must name the PUBLIC_URL source") } // TestValidateHTTPConfig_ProductionRejectsPlainHTTP pins the production // HTTPS policy for the canonical PUBLIC_URL: an explicitly production // worker must not advertise plain HTTP on a non-loopback host. Loopback // and https stay accepted. func TestValidateHTTPConfig_ProductionRejectsPlainHTTP(t *testing.T) { t.Setenv("DEPLOY_ENV", "production") err := ValidateHTTPConfig(HTTPConfig{ Host: "0.0.0.0", Port: 27401, PublicURL: "http://worker.example.com", URLSource: PublicURLSourceCanonical, }, false) require.Error(t, err) assert.Contains(t, err.Error(), "not permitted in production") cfg := HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: "https://worker.example.com", URLSource: PublicURLSourceCanonical} assert.NoError(t, ValidateHTTPConfig(cfg, false)) cfg = HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: "http://localhost:27401", URLSource: PublicURLSourceCanonical} assert.NoError(t, ValidateHTTPConfig(cfg, false)) } // TestValidateHTTPConfig_NonProductionAllowsPlainHTTP keeps the historical // warn-only behavior when no explicit production environment is configured. func TestValidateHTTPConfig_NonProductionAllowsPlainHTTP(t *testing.T) { t.Setenv("DEPLOY_ENV", "") t.Setenv("RSMON_ENV", "development") t.Setenv("GO_ENV", "") cfg := HTTPConfig{Host: "0.0.0.0", Port: 27401, PublicURL: "http://worker.example.com"} assert.NoError(t, ValidateHTTPConfig(cfg, false)) } // TestWarnInsecurePublicURL exercises the warning helper: only http on // non-loopback hosts should warn. The returned host is the parsed host // (host:port when present) so the caller can log a useful target. func TestWarnInsecurePublicURL(t *testing.T) { cases := []struct { name string url string wantWarn bool wantHost string }{ {"empty", "", false, ""}, {"https production", "https://worker.example.com", false, "worker.example.com"}, {"http loopback", "http://localhost:27401", false, "localhost:27401"}, {"http 127.0.0.1", "http://127.0.0.1:27401", false, "127.0.0.1:27401"}, {"http 0.0.0.0", "http://0.0.0.0:27401", false, "0.0.0.0:27401"}, {"http production", "http://worker.example.com", true, "worker.example.com"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { host, warn := WarnInsecurePublicURL(tc.url) assert.Equal(t, tc.wantWarn, warn) assert.Equal(t, tc.wantHost, host) }) } }