package main import ( "errors" "log/slog" "os" "path/filepath" "strings" "testing" "time" "github.com/sophotechlabs/spinoza/internal/debugcontainer" "github.com/sophotechlabs/spinoza/internal/version" ) func TestDefaultsMatchTheDocumentedOnes(t *testing.T) { opts, err := parseFlags(nil) if err != nil { t.Fatalf("parse: %v", err) } if opts.addr != "127.0.0.1:34115" { t.Fatalf("addr = %q", opts.addr) } if opts.cluster.DebugImage != debugcontainer.DefaultImage { t.Fatalf("debug image = %q", opts.cluster.DebugImage) } if opts.cluster.KubectlBinary != debugcontainer.DefaultBinary { t.Fatalf("kubectl = %q", opts.cluster.KubectlBinary) } if opts.cluster.PromSpec != "" { t.Fatalf("prometheus = %q, want discovery", opts.cluster.PromSpec) } if opts.tokenFile != "" { t.Fatalf("token file = %q, want none unless a script asked for one", opts.tokenFile) } } func TestEveryFlagIsCarriedThrough(t *testing.T) { opts, err := parseFlags([]string{ "-addr", "127.0.0.1:9999", "-open", "-debug-image", "ghcr.io/acme/toolbox:2.1", "-kubectl", "/usr/local/bin/kubectl", "-prometheus", "monitoring/prom:9090", "-token-file", "/tmp/spinoza.token", }) if err != nil { t.Fatalf("parse: %v", err) } if opts.addr != "127.0.0.1:9999" { t.Fatalf("addr = %q", opts.addr) } if !opts.openBrowser { t.Fatal("open was dropped") } if opts.tokenFile != "/tmp/spinoza.token" { t.Fatalf("token file = %q", opts.tokenFile) } if opts.cluster.DebugImage != "ghcr.io/acme/toolbox:2.1" { t.Fatalf("debug image = %q", opts.cluster.DebugImage) } if opts.cluster.KubectlBinary != "/usr/local/bin/kubectl" { t.Fatalf("kubectl = %q", opts.cluster.KubectlBinary) } if opts.cluster.PromSpec != "monitoring/prom:9090" { t.Fatalf("prometheus = %q", opts.cluster.PromSpec) } } func TestNodeShellFlagRequiresAPinnedImage(t *testing.T) { _, err := parseFlags([]string{"-node-shell", "-node-shell-image", "busybox:1.37"}) if err == nil || err.Error() != "node-shell-image must be pinned by sha256 digest" { t.Fatalf("error = %v", err) } } func TestTheTokenFileIsWrittenForScriptsOnly(t *testing.T) { path := filepath.Join(t.TempDir(), "token") err := writeTokenFile(path, "s3cret") if err != nil { t.Fatalf("write: %v", err) } data, readErr := os.ReadFile(path) if readErr != nil { t.Fatalf("read: %v", readErr) } if string(data) != "s3cret\n" { t.Fatalf("contents = %q", data) } info, statErr := os.Stat(path) if statErr != nil { t.Fatalf("stat: %v", statErr) } if info.Mode().Perm() != 0o600 { t.Fatalf("mode = %v, want the token readable by its owner only", info.Mode().Perm()) } } func TestNoTokenFileMeansNoFile(t *testing.T) { err := writeTokenFile("", "s3cret") if err != nil { t.Fatalf("write: %v", err) } } func TestAnUnwritableTokenFileIsReported(t *testing.T) { path := filepath.Join(t.TempDir(), "missing", "token") err := writeTokenFile(path, "s3cret") if err == nil { t.Fatal("a token file that cannot be written was reported as fine") } } func TestEveryLogLevelIsAccepted(t *testing.T) { cases := map[string]slog.Level{ "debug": slog.LevelDebug, "info": slog.LevelInfo, "warn": slog.LevelWarn, "error": slog.LevelError, } for name, want := range cases { t.Run(name, func(t *testing.T) { opts, err := parseFlags([]string{"-log-level", name}) if err != nil { t.Fatalf("parse: %v", err) } if opts.logLevel != want { t.Fatalf("level = %v, want %v", opts.logLevel, want) } }) } } func TestAnUnknownLogLevelIsRefused(t *testing.T) { _, err := parseFlags([]string{"-log-level", "chatty"}) if err == nil { t.Fatal("an unknown log level was accepted") } if !strings.Contains(err.Error(), "debug, info, warn, error") { t.Fatalf("err = %v, want it to name the levels", err) } } func TestTheVersionFlagIsCarriedThrough(t *testing.T) { opts, err := parseFlags([]string{"-version"}) if err != nil { t.Fatalf("parse: %v", err) } if !opts.showVersion { t.Fatal("version was dropped") } } func TestTheLicenseFlagIsCarriedThrough(t *testing.T) { opts, err := parseFlags([]string{"-license"}) if err != nil { t.Fatalf("parse: %v", err) } if !opts.showLicense { t.Fatal("license was dropped") } } func TestTheEnvironmentFillsInWhatTheDesktopBundleCannotPass(t *testing.T) { t.Setenv("SPINOZA_ADDR", "127.0.0.1:7777") t.Setenv("SPINOZA_OPEN", "true") t.Setenv("SPINOZA_TOKEN_FILE", "/run/spinoza.token") t.Setenv("SPINOZA_LOG_LEVEL", "debug") t.Setenv("SPINOZA_DEBUG_IMAGE", "ghcr.io/acme/toolbox:9") t.Setenv("SPINOZA_KUBECTL", "/opt/kubectl") t.Setenv("SPINOZA_PROMETHEUS", "obs/prom:9090") opts, err := parseFlags(nil) if err != nil { t.Fatalf("parse: %v", err) } if opts.addr != "127.0.0.1:7777" { t.Fatalf("addr = %q", opts.addr) } if !opts.openBrowser { t.Fatal("open was not read from the environment") } if opts.tokenFile != "/run/spinoza.token" { t.Fatalf("token file = %q", opts.tokenFile) } if opts.logLevel != slog.LevelDebug { t.Fatalf("log level = %v", opts.logLevel) } if opts.cluster.DebugImage != "ghcr.io/acme/toolbox:9" { t.Fatalf("debug image = %q", opts.cluster.DebugImage) } if opts.cluster.KubectlBinary != "/opt/kubectl" { t.Fatalf("kubectl = %q", opts.cluster.KubectlBinary) } if opts.cluster.PromSpec != "obs/prom:9090" { t.Fatalf("prometheus = %q", opts.cluster.PromSpec) } } func TestAFlagBeatsTheEnvironment(t *testing.T) { t.Setenv("SPINOZA_ADDR", "127.0.0.1:7777") opts, err := parseFlags([]string{"-addr", "127.0.0.1:8888"}) if err != nil { t.Fatalf("parse: %v", err) } if opts.addr != "127.0.0.1:8888" { t.Fatalf("addr = %q, want the flag to win", opts.addr) } } func TestAnUnreadableBooleanInTheEnvironmentIsIgnored(t *testing.T) { cases := map[string]bool{ "true": true, "1": true, "false": false, "yes": false, } for value, want := range cases { t.Run(value, func(t *testing.T) { t.Setenv("SPINOZA_OPEN", value) opts, err := parseFlags(nil) if err != nil { t.Fatalf("parse: %v", err) } if opts.openBrowser != want { t.Fatalf("open = %v for %q, want %v", opts.openBrowser, value, want) } }) } } func TestTheBigClusterKnobsKeepTodaysValuesByDefault(t *testing.T) { opts, err := parseFlags(nil) if err != nil { t.Fatalf("parse: %v", err) } if opts.cluster.Kubeconfig != "" { t.Fatalf("kubeconfig = %q, want the usual lookup rules", opts.cluster.Kubeconfig) } if opts.cluster.ClientQPS != defaultQPS { t.Fatalf("qps = %v, want %v", opts.cluster.ClientQPS, float32(defaultQPS)) } if opts.cluster.ClientBurst != defaultBurst { t.Fatalf("burst = %d, want %d", opts.cluster.ClientBurst, defaultBurst) } if opts.cluster.SyncTimeout != defaultSync { t.Fatalf("sync timeout = %s, want %s", opts.cluster.SyncTimeout, defaultSync) } if opts.cluster.WarmConcurrency != defaultWarm { t.Fatalf("warm concurrency = %d, want %d", opts.cluster.WarmConcurrency, defaultWarm) } if opts.cluster.CountBudget != defaultCountBudget { t.Fatalf("count budget = %s, want %s", opts.cluster.CountBudget, defaultCountBudget) } if opts.cluster.CountPerType != defaultCountPerType { t.Fatalf("count timeout = %s, want %s", opts.cluster.CountPerType, defaultCountPerType) } if opts.cluster.CountConcurrency != defaultCountConcurrency { t.Fatalf("count concurrency = %d, want %d", opts.cluster.CountConcurrency, defaultCountConcurrency) } } func TestABigClusterCanRaiseEveryLimit(t *testing.T) { opts, err := parseFlags([]string{ "-kubeconfig", "/etc/spinoza/kubeconfig", "-qps", "200", "-burst", "400", "-sync-timeout", "3m", "-warm-concurrency", "24", "-count-budget", "90s", "-count-timeout", "20s", "-count-concurrency", "48", }) if err != nil { t.Fatalf("parse: %v", err) } if opts.cluster.Kubeconfig != "/etc/spinoza/kubeconfig" { t.Fatalf("kubeconfig = %q", opts.cluster.Kubeconfig) } if opts.cluster.ClientQPS != 200 { t.Fatalf("qps = %v", opts.cluster.ClientQPS) } if opts.cluster.ClientBurst != 400 { t.Fatalf("burst = %d", opts.cluster.ClientBurst) } if opts.cluster.SyncTimeout != 3*time.Minute { t.Fatalf("sync timeout = %s", opts.cluster.SyncTimeout) } if opts.cluster.WarmConcurrency != 24 { t.Fatalf("warm concurrency = %d", opts.cluster.WarmConcurrency) } if opts.cluster.CountBudget != 90*time.Second { t.Fatalf("count budget = %s", opts.cluster.CountBudget) } if opts.cluster.CountPerType != 20*time.Second { t.Fatalf("count timeout = %s", opts.cluster.CountPerType) } if opts.cluster.CountConcurrency != 48 { t.Fatalf("count concurrency = %d", opts.cluster.CountConcurrency) } } func TestNonPositiveBigClusterLimitsAreRefused(t *testing.T) { cases := [][]string{ {"-qps", "0"}, {"-qps", "NaN"}, {"-qps", "+Inf"}, {"-burst", "0"}, {"-sync-timeout", "0s"}, {"-warm-concurrency", "-1"}, {"-count-budget", "-1s"}, {"-count-timeout", "0s"}, {"-count-concurrency", "-1"}, } for _, args := range cases { t.Run(strings.Join(args, "="), func(t *testing.T) { if _, err := parseFlags(args); err == nil { t.Fatalf("arguments %v were accepted", args) } }) } } func TestTheEnvironmentCanRaiseTheLimitsToo(t *testing.T) { t.Setenv("SPINOZA_KUBECONFIG", "/tmp/kubeconfig") t.Setenv("SPINOZA_QPS", "150") t.Setenv("SPINOZA_BURST", "300") t.Setenv("SPINOZA_SYNC_TIMEOUT", "2m") t.Setenv("SPINOZA_COUNT_CONCURRENCY", "36") opts, err := parseFlags(nil) if err != nil { t.Fatalf("parse: %v", err) } if opts.cluster.Kubeconfig != "/tmp/kubeconfig" { t.Fatalf("kubeconfig = %q", opts.cluster.Kubeconfig) } if opts.cluster.ClientQPS != 150 { t.Fatalf("qps = %v", opts.cluster.ClientQPS) } if opts.cluster.ClientBurst != 300 { t.Fatalf("burst = %d", opts.cluster.ClientBurst) } if opts.cluster.SyncTimeout != 2*time.Minute { t.Fatalf("sync timeout = %s", opts.cluster.SyncTimeout) } if opts.cluster.CountConcurrency != 36 { t.Fatalf("count concurrency = %d", opts.cluster.CountConcurrency) } } func TestUnreadableLimitsInTheEnvironmentFallBack(t *testing.T) { t.Setenv("SPINOZA_QPS", "fast") t.Setenv("SPINOZA_BURST", "lots") t.Setenv("SPINOZA_SYNC_TIMEOUT", "ages") opts, err := parseFlags(nil) if err != nil { t.Fatalf("parse: %v", err) } if opts.cluster.ClientQPS != defaultQPS { t.Fatalf("qps = %v, want the default", opts.cluster.ClientQPS) } if opts.cluster.ClientBurst != defaultBurst { t.Fatalf("burst = %d, want the default", opts.cluster.ClientBurst) } if opts.cluster.SyncTimeout != defaultSync { t.Fatalf("sync timeout = %s, want the default", opts.cluster.SyncTimeout) } } func TestAnUnknownFlagIsAnError(t *testing.T) { _, err := parseFlags([]string{"-nope"}) if err == nil { t.Fatal("an unknown flag was accepted") } } func TestHelpIsNotAFailure(t *testing.T) { _, err := parseFlags([]string{"-h"}) if !errors.Is(err, errHelp) { t.Fatalf("err = %v, want the help sentinel so the process can exit 0", err) } } func TestABadFlagIsStillAFailure(t *testing.T) { _, err := parseFlags([]string{"-nonsense"}) if err == nil { t.Fatal("an unknown flag was accepted") } if errors.Is(err, errHelp) { t.Fatal("an unknown flag was treated as a help request") } } func TestSettingsComeFromTheProcessArguments(t *testing.T) { original := os.Args t.Cleanup(func() { os.Args = original }) os.Args = []string{"spinoza", "--addr", "127.0.0.1:9999", "--log-level", "debug"} result, err := settingsFromArgs() if err != nil { t.Fatalf("settingsFromArgs: %v", err) } if result.addr != "127.0.0.1:9999" { t.Fatalf("addr = %q, want the one the arguments named", result.addr) } if result.logLevel != slog.LevelDebug { t.Fatalf("log level = %v, want debug", result.logLevel) } } func TestSettingsRefuseAnArgumentTheyDoNotKnow(t *testing.T) { original := os.Args t.Cleanup(func() { os.Args = original }) os.Args = []string{"spinoza", "--nonsense"} _, err := settingsFromArgs() if err == nil { t.Fatal("settingsFromArgs returned nil error for an unknown flag") } } func TestTheCheckerCarriesThisBuildsVersion(t *testing.T) { checker := updateChecker() if got := checker.Status(t.Context()).Current; got != version.String() { t.Fatalf("current = %q, want this build's version", got) } }