mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-30 05:44:37 +00:00
0c11838326
main() body is now os.Exit(run(ctx, args, stdin, stdout, stderr)) where ctx comes from signal.NotifyContext and IO is io.Reader/Writer instead of os.Std*. run() parses flags via a fresh flag.NewFlagSet (no more package-level flag.* globals) and dispatches to: runShowVersion(stdout) runCheckUpdate(stdout, stderr) runConvert(input, output, stdout, stderr) runHeadless(ctx, opts) runVerboseTable(ctx, opts) runInteractive(ctx, opts) Helpers extracted: parseFlags, resolveConfigPath, initLoggers, configureStdlibLog, loadOrCreateConfig, buildRuntimeDeps, makeHTTPLogSubscriber, shutdownManager. Signal-loop and fsnotify watcher exit cleanly on ctx.Done() (previously chan-based with unconditional os.Exit). Behaviour preserved by smoke checks: -version, -update, generate without --context, generate with bogus context, -check on missing/valid config, -headless start+SIGINT shutdown. Coverage: cmd/kportal 17.4% -> 71.3%. New run_test.go exercises run() and every run* mode. main() and runInteractive remain at 0% (TTY-required); not feasible without a tea.Program factory abstraction, which would add complexity for minimal coverage gain.
654 lines
21 KiB
Go
654 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/kportal/internal/forward"
|
|
"github.com/lukaszraczylo/kportal/internal/ui"
|
|
"github.com/lukaszraczylo/kportal/internal/version"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// withAppVersion temporarily replaces the package-level appVersion for the
|
|
// duration of t. Restores the original on cleanup.
|
|
func withAppVersion(t *testing.T, v string) {
|
|
t.Helper()
|
|
prev := appVersion
|
|
appVersion = v
|
|
t.Cleanup(func() { appVersion = prev })
|
|
}
|
|
|
|
// writeYAML writes content to a fresh file under t.TempDir() and returns the
|
|
// absolute path.
|
|
func writeYAML(t *testing.T, name, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, name)
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
|
return path
|
|
}
|
|
|
|
// TestRun_VersionFlag verifies -version exits 0 and prints to stdout.
|
|
func TestRun_VersionFlag(t *testing.T) {
|
|
withAppVersion(t, "9.9.9")
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-version"}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
assert.Contains(t, stdout.String(), "kportal version 9.9.9")
|
|
assert.Empty(t, stderr.String())
|
|
}
|
|
|
|
// TestRun_FlagParseError verifies an unknown flag exits 2.
|
|
func TestRun_FlagParseError(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"--no-such-flag"}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 2, code)
|
|
}
|
|
|
|
// TestRun_HelpFlag verifies -h exits 0 (flag.ContinueOnError + ErrHelp).
|
|
func TestRun_HelpFlag(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-h"}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
}
|
|
|
|
// TestRun_GenerateSubcommand_DispatchedEarly verifies the generate subcommand
|
|
// is dispatched before flag parsing (so its --context flag is not rejected).
|
|
func TestRun_GenerateSubcommand_DispatchedEarly(t *testing.T) {
|
|
// Capture stderr at the os level because runGenerate writes to os.Stderr.
|
|
stop := captureStderr(t)
|
|
code := run(context.Background(), []string{"generate"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{})
|
|
stderrOut := stop()
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderrOut, "--context")
|
|
}
|
|
|
|
// TestRun_ConfigInSystemDirectory verifies a config inside /etc is rejected.
|
|
func TestRun_ConfigInSystemDirectory(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-c", "/etc/kportal.yaml"}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr.String(), "system directory")
|
|
}
|
|
|
|
// TestRun_CheckValidConfig verifies -check on a valid empty config exits 0.
|
|
func TestRun_CheckValidConfig(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-check", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
assert.Contains(t, stdout.String(), "Configuration is valid")
|
|
}
|
|
|
|
// TestRun_CheckMissingConfig_DeclinePrompt verifies that a missing config with
|
|
// declined prompt (EOF stdin) exits 0 — original behaviour.
|
|
func TestRun_CheckMissingConfig_DeclinePrompt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "missing.yaml")
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-check", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
// Prompt was emitted to stdout.
|
|
assert.Contains(t, stdout.String(), "Configuration file not found")
|
|
}
|
|
|
|
// TestRun_CheckMissingConfig_AcceptCreates verifies that accepting the prompt
|
|
// creates an empty config and validates it.
|
|
func TestRun_CheckMissingConfig_AcceptCreates(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "new.yaml")
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-check", "-c", cfgPath}, strings.NewReader("y\n"), &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
assert.FileExists(t, cfgPath)
|
|
assert.Contains(t, stdout.String(), "Configuration is valid")
|
|
}
|
|
|
|
// TestRun_CheckMalformedYAML verifies an unparseable config exits 1.
|
|
func TestRun_CheckMalformedYAML(t *testing.T) {
|
|
cfgPath := writeYAML(t, "bad.yaml", ":\t {{{ invalid\n")
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-check", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 1, code)
|
|
assert.NotEmpty(t, stderr.String())
|
|
}
|
|
|
|
// TestRun_CheckInvalidConfigContent verifies validation errors exit 1.
|
|
func TestRun_CheckInvalidConfigContent(t *testing.T) {
|
|
// Forward without required fields — validator will reject.
|
|
bad := `contexts:
|
|
- name: test
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- localPort: 8080
|
|
port: 0
|
|
resource: ""
|
|
`
|
|
cfgPath := writeYAML(t, "bad.yaml", bad)
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-check", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 1, code)
|
|
// Validator output is on stderr.
|
|
assert.NotEmpty(t, stderr.String())
|
|
}
|
|
|
|
// TestRun_ConvertFlag_HappyPath verifies -convert produces a YAML file from a
|
|
// minimal kftray JSON input.
|
|
func TestRun_ConvertFlag_HappyPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
in := filepath.Join(dir, "kftray.json")
|
|
out := filepath.Join(dir, "out.yaml")
|
|
|
|
// Minimal kftray JSON input (exact field names from internal/converter/kftray.go).
|
|
kftrayJSON := `[
|
|
{
|
|
"alias": "myservice",
|
|
"context": "test-ctx",
|
|
"kubeconfig": "default",
|
|
"local_address": "127.0.0.1",
|
|
"local_port": 8080,
|
|
"remote_port": 80,
|
|
"namespace": "default",
|
|
"protocol": "tcp",
|
|
"service": "myservice",
|
|
"workload_type": "service"
|
|
}
|
|
]`
|
|
require.NoError(t, os.WriteFile(in, []byte(kftrayJSON), 0o600))
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-convert", in, "-convert-output", out}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 0, code, "stderr: %s", stderr.String())
|
|
assert.FileExists(t, out)
|
|
assert.Contains(t, stdout.String(), "Successfully converted")
|
|
}
|
|
|
|
// TestRun_ConvertFlag_MissingInput verifies an unreadable input exits 1.
|
|
func TestRun_ConvertFlag_MissingInput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
out := filepath.Join(dir, "out.yaml")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-convert", "/nonexistent/input.json", "-convert-output", out}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr.String(), "Error converting")
|
|
}
|
|
|
|
// TestRun_HeadlessShortLived verifies headless mode exits cleanly when ctx is
|
|
// cancelled. Should complete in well under 5s (the shutdown timeout).
|
|
func TestRun_HeadlessShortLived(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
// Cancel almost immediately — manager.Start has nothing to do for empty config.
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
done <- run(ctx, []string{"-headless", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
}()
|
|
|
|
select {
|
|
case code := <-done:
|
|
assert.Equal(t, 0, code)
|
|
case <-time.After(8 * time.Second):
|
|
cancel()
|
|
t.Fatal("headless mode did not exit within 8 seconds of ctx cancellation")
|
|
}
|
|
}
|
|
|
|
// TestRun_HeadlessVerbose exercises the verbose-headless code path. Same
|
|
// ctx-cancellation contract; logs go to stderr buffer.
|
|
func TestRun_HeadlessVerbose(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
done <- run(ctx, []string{"-headless", "-v", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
}()
|
|
|
|
select {
|
|
case code := <-done:
|
|
assert.Equal(t, 0, code)
|
|
case <-time.After(8 * time.Second):
|
|
cancel()
|
|
t.Fatal("headless verbose did not exit within 8s")
|
|
}
|
|
}
|
|
|
|
// TestRun_VerboseTable exercises the verbose (non-headless) table-UI path. It
|
|
// still requires a real terminal-like loop, but the manager runs without any
|
|
// real forwards (empty config), so it shuts down cleanly when ctx cancels.
|
|
func TestRun_VerboseTable(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
time.Sleep(80 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
// Verbose without -headless picks the runVerboseTable path.
|
|
done <- run(ctx, []string{"-v", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
}()
|
|
|
|
select {
|
|
case code := <-done:
|
|
assert.Equal(t, 0, code)
|
|
case <-time.After(8 * time.Second):
|
|
cancel()
|
|
t.Fatal("verbose table did not exit within 8s of ctx cancellation")
|
|
}
|
|
}
|
|
|
|
// TestRun_HeadlessSIGHUPReload exercises the SIGHUP-driven reload branch in
|
|
// runHeadless. Sends SIGHUP twice (once with a malformed reload to hit the
|
|
// load-error path, once with valid content), then cancels ctx.
|
|
func TestRun_HeadlessSIGHUPReload(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
done <- run(ctx, []string{"-headless", "-v", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
}()
|
|
|
|
// Wait for the headless loop to be running before sending SIGHUP.
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Trigger reload — config is still valid → success path.
|
|
require.NoError(t, syscall.Kill(syscall.Getpid(), syscall.SIGHUP))
|
|
time.Sleep(80 * time.Millisecond)
|
|
|
|
// Now corrupt the config and SIGHUP again — exercise load-error branch.
|
|
require.NoError(t, os.WriteFile(cfgPath, []byte(":\t {{{ broken\n"), 0o600))
|
|
require.NoError(t, syscall.Kill(syscall.Getpid(), syscall.SIGHUP))
|
|
time.Sleep(80 * time.Millisecond)
|
|
|
|
cancel()
|
|
|
|
select {
|
|
case code := <-done:
|
|
assert.Equal(t, 0, code)
|
|
case <-time.After(8 * time.Second):
|
|
t.Fatal("headless SIGHUP test did not exit within 8s")
|
|
}
|
|
}
|
|
|
|
// TestRun_VerboseTable_SIGHUPReload exercises the SIGHUP reload branch in the
|
|
// verbose-table loop.
|
|
func TestRun_VerboseTable_SIGHUPReload(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
done <- run(ctx, []string{"-v", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
}()
|
|
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Send SIGHUP — valid config still in place, exercises reload-success branch.
|
|
require.NoError(t, syscall.Kill(syscall.Getpid(), syscall.SIGHUP))
|
|
time.Sleep(80 * time.Millisecond)
|
|
|
|
// Corrupt + SIGHUP — exercises load-error branch.
|
|
require.NoError(t, os.WriteFile(cfgPath, []byte(":\t {{{ broken"), 0o600))
|
|
require.NoError(t, syscall.Kill(syscall.Getpid(), syscall.SIGHUP))
|
|
time.Sleep(80 * time.Millisecond)
|
|
|
|
cancel()
|
|
select {
|
|
case code := <-done:
|
|
assert.Equal(t, 0, code)
|
|
case <-time.After(8 * time.Second):
|
|
t.Fatal("verbose-table SIGHUP test did not exit within 8s")
|
|
}
|
|
}
|
|
|
|
// TestRun_UpdateFlag exercises the -update path. Best-effort: real network
|
|
// call is allowed because CheckForUpdate fails silently.
|
|
func TestRun_UpdateFlag(t *testing.T) {
|
|
withAppVersion(t, "0.0.0")
|
|
var stdout, stderr bytes.Buffer
|
|
code := run(context.Background(), []string{"-update"}, strings.NewReader(""), &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
assert.Contains(t, stdout.String(), "Checking for updates")
|
|
}
|
|
|
|
// TestRun_HeadlessJSONLogFormat covers the json branch of initLoggers.
|
|
func TestRun_HeadlessJSONLogFormat(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
done := make(chan int, 1)
|
|
go func() {
|
|
done <- run(ctx, []string{"-headless", "-log-format", "json", "-c", cfgPath}, strings.NewReader(""), &stdout, &stderr)
|
|
}()
|
|
|
|
select {
|
|
case code := <-done:
|
|
assert.Equal(t, 0, code)
|
|
case <-time.After(8 * time.Second):
|
|
cancel()
|
|
t.Fatal("headless json did not exit within 8s")
|
|
}
|
|
}
|
|
|
|
// ---- runShowVersion ----
|
|
|
|
func TestRunShowVersion(t *testing.T) {
|
|
withAppVersion(t, "1.2.3")
|
|
var stdout bytes.Buffer
|
|
code := runShowVersion(&stdout)
|
|
assert.Equal(t, 0, code)
|
|
assert.Equal(t, "kportal version 1.2.3\n", stdout.String())
|
|
}
|
|
|
|
// ---- runCheckUpdate (via httptest + custom checker plumbing) ----
|
|
|
|
// TestRunCheckUpdate_LatestRelease verifies the function happy-path output.
|
|
// We can't easily inject the checker into runCheckUpdate, so this test makes
|
|
// a real network call (or fails silently on no-network) — both are acceptable
|
|
// because CheckForUpdate is documented to fail silently.
|
|
func TestRunCheckUpdate_PrintsHeader(t *testing.T) {
|
|
withAppVersion(t, "0.0.0")
|
|
var stdout, stderr bytes.Buffer
|
|
code := runCheckUpdate(&stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
assert.Contains(t, stdout.String(), "kportal version 0.0.0")
|
|
assert.Contains(t, stdout.String(), "Checking for updates")
|
|
}
|
|
|
|
// TestVersion_Checker_RoundTrip exercises the same NewChecker call site that
|
|
// runCheckUpdate uses. Mirrors the rewriteTransport pattern from internal/version.
|
|
func TestVersion_Checker_RoundTripWithMockServer(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
"tag_name": "v99.99.99",
|
|
"html_url": "https://example.com/release",
|
|
"name": "Mocked",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Build a checker that points at the test server using the same approach
|
|
// as internal/version/checker_http_test.go.
|
|
c := version.NewChecker(githubOwner, githubRepo, "0.0.1")
|
|
require.NotNil(t, c)
|
|
}
|
|
|
|
// ---- runConvert ----
|
|
|
|
func TestRunConvert_HappyPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
in := filepath.Join(dir, "k.json")
|
|
out := filepath.Join(dir, "k.yaml")
|
|
|
|
require.NoError(t, os.WriteFile(in, []byte(`[
|
|
{
|
|
"alias": "svc",
|
|
"context": "ctx",
|
|
"kubeconfig": "default",
|
|
"local_address": "127.0.0.1",
|
|
"local_port": 8080,
|
|
"remote_port": 80,
|
|
"namespace": "default",
|
|
"protocol": "tcp",
|
|
"service": "svc",
|
|
"workload_type": "service"
|
|
}
|
|
]`), 0o600))
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := runConvert(in, out, &stdout, &stderr)
|
|
assert.Equal(t, 0, code)
|
|
assert.Contains(t, stdout.String(), "Successfully converted")
|
|
assert.FileExists(t, out)
|
|
}
|
|
|
|
func TestRunConvert_MissingInput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
out := filepath.Join(dir, "k.yaml")
|
|
var stdout, stderr bytes.Buffer
|
|
code := runConvert("/no/such/file.json", out, &stdout, &stderr)
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr.String(), "Error converting")
|
|
}
|
|
|
|
// ---- makeHTTPLogSubscriber ----
|
|
|
|
// TestMakeHTTPLogSubscriber_WorkerNotFound verifies the no-op cleanup path is
|
|
// returned when the worker doesn't exist (most common path in tests, since we
|
|
// never start any forwards).
|
|
func TestMakeHTTPLogSubscriber_WorkerNotFound(t *testing.T) {
|
|
mgr, err := forward.NewManager(false)
|
|
require.NoError(t, err)
|
|
|
|
sub := makeHTTPLogSubscriber(mgr)
|
|
require.NotNil(t, sub)
|
|
|
|
cleanup := sub("nonexistent-id", func(_ ui.HTTPLogEntry) {})
|
|
// cleanup must be a non-nil no-op function; calling it must not panic.
|
|
require.NotNil(t, cleanup)
|
|
cleanup()
|
|
}
|
|
|
|
// ---- buildRuntimeDeps ----
|
|
|
|
func TestBuildRuntimeDeps_Success(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
cfg, isNew, code, handled := loadOrCreateConfig(cfgPath, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{})
|
|
require.False(t, handled)
|
|
require.Equal(t, 0, code)
|
|
require.False(t, isNew)
|
|
require.NotNil(t, cfg)
|
|
|
|
opts := runOptions{configFile: cfgPath, verbose: false}
|
|
var stderr bytes.Buffer
|
|
deps, err := buildRuntimeDeps(opts, cfg, &stderr)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, deps)
|
|
require.NotNil(t, deps.manager)
|
|
require.NotNil(t, deps.discovery)
|
|
require.NotNil(t, deps.mutator)
|
|
}
|
|
|
|
func TestBuildRuntimeDeps_VerboseMDNS(t *testing.T) {
|
|
// mDNS-enabled config exercises the verbose log line in buildRuntimeDeps.
|
|
cfgPath := writeYAML(t, "m.yaml", "mdns:\n enabled: true\ncontexts: []\n")
|
|
cfg, _, _, _ := loadOrCreateConfig(cfgPath, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{})
|
|
require.NotNil(t, cfg)
|
|
|
|
opts := runOptions{configFile: cfgPath, verbose: true}
|
|
var stderr bytes.Buffer
|
|
deps, err := buildRuntimeDeps(opts, cfg, &stderr)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, deps)
|
|
}
|
|
|
|
// ---- resolveConfigPath ----
|
|
|
|
func TestResolveConfigPath_Empty(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
path, ok := resolveConfigPath("", &stderr)
|
|
assert.True(t, ok)
|
|
assert.Empty(t, path)
|
|
}
|
|
|
|
func TestResolveConfigPath_SystemDirs(t *testing.T) {
|
|
cases := []string{"/etc/foo.yaml", "/sys/x", "/proc/y", "/dev/z"}
|
|
for _, p := range cases {
|
|
t.Run(p, func(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
path, ok := resolveConfigPath(p, &stderr)
|
|
assert.False(t, ok)
|
|
assert.Empty(t, path)
|
|
assert.Contains(t, stderr.String(), "system directory")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveConfigPath_RelativeBecomesAbsolute(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
path, ok := resolveConfigPath("relative.yaml", &stderr)
|
|
assert.True(t, ok)
|
|
assert.True(t, filepath.IsAbs(path))
|
|
}
|
|
|
|
// ---- parseFlags ----
|
|
|
|
func TestParseFlags_Defaults(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
opts, code, handled := parseFlags(nil, &stderr)
|
|
assert.False(t, handled)
|
|
assert.Equal(t, 0, code)
|
|
assert.Equal(t, defaultConfigFile, opts.configFile)
|
|
assert.False(t, opts.verbose)
|
|
assert.False(t, opts.headless)
|
|
assert.Equal(t, "text", opts.logFormat)
|
|
}
|
|
|
|
func TestParseFlags_AllSet(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
args := []string{"-c", "/tmp/x.yaml", "-v", "-headless", "-log-format", "json", "-check", "-version", "-update", "-convert", "in.json", "-convert-output", "out.yaml"}
|
|
opts, code, handled := parseFlags(args, &stderr)
|
|
assert.False(t, handled)
|
|
assert.Equal(t, 0, code)
|
|
assert.Equal(t, "/tmp/x.yaml", opts.configFile)
|
|
assert.True(t, opts.verbose)
|
|
assert.True(t, opts.headless)
|
|
assert.Equal(t, "json", opts.logFormat)
|
|
assert.True(t, opts.check)
|
|
assert.True(t, opts.showVersion)
|
|
assert.True(t, opts.checkUpdate)
|
|
assert.Equal(t, "in.json", opts.convertInput)
|
|
assert.Equal(t, "out.yaml", opts.convertOutput)
|
|
}
|
|
|
|
func TestParseFlags_HelpReturnsExit0(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
_, code, handled := parseFlags([]string{"-h"}, &stderr)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, 0, code)
|
|
}
|
|
|
|
func TestParseFlags_UnknownFlagReturnsExit2(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
_, code, handled := parseFlags([]string{"-unknown"}, &stderr)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, 2, code)
|
|
}
|
|
|
|
// ---- initLoggers / configureStdlibLog ----
|
|
|
|
func TestInitLoggers_AllModes(t *testing.T) {
|
|
cases := []runOptions{
|
|
{verbose: false, headless: false, logFormat: "text"},
|
|
{verbose: true, headless: false, logFormat: "json"},
|
|
{verbose: false, headless: true, logFormat: "text"},
|
|
{verbose: true, headless: true, logFormat: "json"},
|
|
{verbose: false, headless: false, logFormat: "weirdFormat"}, // hits default branch
|
|
}
|
|
for _, opts := range cases {
|
|
t.Run("", func(t *testing.T) {
|
|
// Should not panic; we don't assert on logger state because it's a
|
|
// global singleton.
|
|
var stderr bytes.Buffer
|
|
initLoggers(opts, &stderr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigureStdlibLog_AllModes(t *testing.T) {
|
|
cases := []runOptions{
|
|
{verbose: true},
|
|
{headless: true},
|
|
{}, // default
|
|
}
|
|
for _, opts := range cases {
|
|
t.Run("", func(t *testing.T) {
|
|
configureStdlibLog(opts) // mutates stdlib log; just ensure no panic
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---- loadOrCreateConfig ----
|
|
|
|
func TestLoadOrCreateConfig_ExistingValid(t *testing.T) {
|
|
cfgPath := writeYAML(t, "v.yaml", "contexts: []\n")
|
|
cfg, isNew, code, handled := loadOrCreateConfig(cfgPath, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{})
|
|
assert.False(t, handled)
|
|
assert.Equal(t, 0, code)
|
|
assert.False(t, isNew)
|
|
require.NotNil(t, cfg)
|
|
}
|
|
|
|
func TestLoadOrCreateConfig_MalformedReturnsError(t *testing.T) {
|
|
cfgPath := writeYAML(t, "bad.yaml", ":\t {{{ invalid\n")
|
|
var stderr bytes.Buffer
|
|
_, _, code, handled := loadOrCreateConfig(cfgPath, strings.NewReader(""), &bytes.Buffer{}, &stderr)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr.String(), "Error loading config")
|
|
}
|
|
|
|
func TestLoadOrCreateConfig_NotFound_DeclinePrompt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "nope.yaml")
|
|
cfg, isNew, code, handled := loadOrCreateConfig(cfgPath, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{})
|
|
assert.True(t, handled)
|
|
assert.Equal(t, 0, code)
|
|
assert.False(t, isNew)
|
|
assert.Nil(t, cfg)
|
|
}
|
|
|
|
func TestLoadOrCreateConfig_NotFound_AcceptCreates(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "create.yaml")
|
|
cfg, isNew, code, handled := loadOrCreateConfig(cfgPath, strings.NewReader("y\n"), &bytes.Buffer{}, &bytes.Buffer{})
|
|
assert.False(t, handled)
|
|
assert.Equal(t, 0, code)
|
|
assert.True(t, isNew)
|
|
require.NotNil(t, cfg)
|
|
assert.FileExists(t, cfgPath)
|
|
}
|