238 lines
6.9 KiB
Go
238 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
limenexec "github.com/KofTwentyTwo/limen/internal/exec"
|
|
"github.com/KofTwentyTwo/limen/internal/ui"
|
|
)
|
|
|
|
func TestRunLoadsConfigStateAndHandsOffSelectedPlan(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configPath := filepath.Join(dir, "hosts.json")
|
|
statePath := filepath.Join(dir, "state.json")
|
|
|
|
if err := os.WriteFile(configPath, []byte(`{
|
|
"hosts": [
|
|
{"name": "prod", "hostname": "prod.example.com", "user": "deploy"}
|
|
]
|
|
}`), 0o600); err != nil {
|
|
t.Fatalf("write config fixture: %v", err)
|
|
}
|
|
if err := os.WriteFile(statePath, []byte(`{"lastAttached":{"prod":"2026-05-26T13:45:00Z"}}`), 0o600); err != nil {
|
|
t.Fatalf("write state fixture: %v", err)
|
|
}
|
|
|
|
wantPlan := limenexec.Plan{HostName: "prod", Argv: []string{"ssh", "-t", "prod.example.com", "tmux", "new"}}
|
|
var sawHosts int
|
|
var sawStateEntries int
|
|
var handedPlan limenexec.Plan
|
|
var handedOptions limenexec.Options
|
|
|
|
code := runWithDeps([]string{"--config", configPath, "--state", statePath}, &bytes.Buffer{}, &bytes.Buffer{}, appDeps{
|
|
runUI: func(app ui.App) (ui.Result, error) {
|
|
sawHosts = len(app.Config.Hosts)
|
|
sawStateEntries = len(app.State.LastAttached)
|
|
return ui.Result{Plan: wantPlan, HasPlan: true}, nil
|
|
},
|
|
handoff: func(plan limenexec.Plan, opts limenexec.Options) int {
|
|
handedPlan = plan
|
|
handedOptions = opts
|
|
return 0
|
|
},
|
|
})
|
|
|
|
if code != 0 {
|
|
t.Fatalf("run exit code = %d, want 0", code)
|
|
}
|
|
if sawHosts != 1 {
|
|
t.Fatalf("UI saw %d hosts, want 1", sawHosts)
|
|
}
|
|
if sawStateEntries != 1 {
|
|
t.Fatalf("UI saw %d state entries, want 1", sawStateEntries)
|
|
}
|
|
if !reflect.DeepEqual(handedPlan, wantPlan) {
|
|
t.Fatalf("handoff plan = %#v, want %#v", handedPlan, wantPlan)
|
|
}
|
|
if handedOptions.StatePath != statePath {
|
|
t.Fatalf("handoff state path = %q, want %q", handedOptions.StatePath, statePath)
|
|
}
|
|
}
|
|
|
|
func TestRunPrintsResultMessageBeforeHandoff(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "hosts.json")
|
|
if err := os.WriteFile(configPath, []byte(`{"hosts":[]}`), 0o600); err != nil {
|
|
t.Fatalf("write config fixture: %v", err)
|
|
}
|
|
|
|
var stderr bytes.Buffer
|
|
code := runWithDeps([]string{"--config", configPath, "--state", filepath.Join(t.TempDir(), "state.json")}, &bytes.Buffer{}, &stderr, appDeps{
|
|
runUI: func(app ui.App) (ui.Result, error) {
|
|
return ui.Result{
|
|
Plan: limenexec.Plan{HostName: "localhost", Argv: []string{"tmux", "new"}},
|
|
HasPlan: true,
|
|
Message: "terminal too small for limen UI; falling back to bare tmux new",
|
|
}, nil
|
|
},
|
|
handoff: func(plan limenexec.Plan, opts limenexec.Options) int {
|
|
return 0
|
|
},
|
|
})
|
|
|
|
if code != 0 {
|
|
t.Fatalf("run exit code = %d, want 0", code)
|
|
}
|
|
want := "terminal too small for limen UI; falling back to bare tmux new\n"
|
|
if stderr.String() != want {
|
|
t.Fatalf("stderr = %q, want %q", stderr.String(), want)
|
|
}
|
|
}
|
|
|
|
func TestRunWarnsForMissingConfigAndStillStartsUI(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "hosts.json")
|
|
statePath := filepath.Join(t.TempDir(), "state.json")
|
|
var stderr bytes.Buffer
|
|
var sawHosts int
|
|
|
|
code := runWithDeps([]string{"--config", configPath, "--state", statePath}, &bytes.Buffer{}, &stderr, appDeps{
|
|
runUI: func(app ui.App) (ui.Result, error) {
|
|
sawHosts = len(app.Config.Hosts)
|
|
return ui.Result{ExitCode: 0}, nil
|
|
},
|
|
handoff: func(plan limenexec.Plan, opts limenexec.Options) int {
|
|
t.Fatal("handoff should not be called")
|
|
return 1
|
|
},
|
|
})
|
|
|
|
if code != 0 {
|
|
t.Fatalf("run exit code = %d, want 0; stderr = %q", code, stderr.String())
|
|
}
|
|
wantWarning := "limen: no hosts.json found at " + configPath + "; only localhost will be available\n"
|
|
if stderr.String() != wantWarning {
|
|
t.Fatalf("stderr = %q, want %q", stderr.String(), wantWarning)
|
|
}
|
|
if sawHosts != 0 {
|
|
t.Fatalf("UI saw %d configured hosts, want 0", sawHosts)
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsFailureForConfigErrors(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "hosts.json")
|
|
if err := os.WriteFile(configPath, []byte(`{"hosts":[{"name":"localhost","hostname":"127.0.0.1"}]}`), 0o600); err != nil {
|
|
t.Fatalf("write config fixture: %v", err)
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := runWithDeps([]string{"--config", configPath, "--state", filepath.Join(t.TempDir(), "state.json")}, &stdout, &stderr, appDeps{})
|
|
|
|
if code != 1 {
|
|
t.Fatalf("run exit code = %d, want 1", code)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
}
|
|
|
|
want := "limen: \"localhost\" is reserved\n"
|
|
if stderr.String() != want {
|
|
t.Fatalf("stderr = %q, want %q", stderr.String(), want)
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsUsageFailureForFlagErrors(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := run([]string{"--config"}, &stdout, &stderr)
|
|
|
|
if code != 2 {
|
|
t.Fatalf("run exit code = %d, want 2", code)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
}
|
|
if got := stderr.String(); got == "" {
|
|
t.Fatal("stderr is empty, want flag error")
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsUsageFailureForUnexpectedArguments(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := run([]string{"extra"}, &stdout, &stderr)
|
|
|
|
if code != 2 {
|
|
t.Fatalf("run exit code = %d, want 2", code)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
}
|
|
|
|
want := "limen: unexpected argument \"extra\"\n"
|
|
if stderr.String() != want {
|
|
t.Fatalf("stderr = %q, want %q", stderr.String(), want)
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsVersion(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
code := run([]string{"--version"}, &stdout, &stderr)
|
|
|
|
if code != 0 {
|
|
t.Fatalf("run exit code = %d, want 0", code)
|
|
}
|
|
if stdout.String() != version+"\n" {
|
|
t.Fatalf("stdout = %q, want version", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("stderr = %q, want empty", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsHelp(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
code := run([]string{"--help"}, &stdout, &stderr)
|
|
|
|
if code != 0 {
|
|
t.Fatalf("run exit code = %d, want 0", code)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "Usage of limen") {
|
|
t.Fatalf("stderr = %q, want usage", got)
|
|
}
|
|
}
|
|
|
|
func TestRunReturnsUIFailure(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "hosts.json")
|
|
if err := os.WriteFile(configPath, []byte(`{"hosts":[]}`), 0o600); err != nil {
|
|
t.Fatalf("write config fixture: %v", err)
|
|
}
|
|
|
|
var stderr bytes.Buffer
|
|
code := runWithDeps([]string{"--config", configPath, "--state", filepath.Join(t.TempDir(), "state.json")}, &bytes.Buffer{}, &stderr, appDeps{
|
|
runUI: func(app ui.App) (ui.Result, error) {
|
|
return ui.Result{}, errors.New("terminal unavailable")
|
|
},
|
|
})
|
|
|
|
if code != 1 {
|
|
t.Fatalf("run exit code = %d, want 1", code)
|
|
}
|
|
want := "limen: ui failed: terminal unavailable\n"
|
|
if stderr.String() != want {
|
|
t.Fatalf("stderr = %q, want %q", stderr.String(), want)
|
|
}
|
|
}
|