Files
worker/internal/distworker/config_test.go
Gleb Tv cb23f123ae
Все проверки выполнены успешно
CI / test (push) Successful in 10m15s
Docker / Build and publish worker image (push) Successful in 34m59s
feat(worker): adopt canonical public URL
2026-08-12 20:48:01 +03:00

382 строки
14 KiB
Go

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