629 lines
14 KiB
Go
629 lines
14 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"bee/audit/internal/app"
|
|
"bee/audit/internal/platform"
|
|
"bee/audit/internal/runtimeenv"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
func newTestModel() model {
|
|
return newModel(app.New(platform.New()), runtimeenv.ModeLocal)
|
|
}
|
|
|
|
func sendKey(t *testing.T, m model, key tea.KeyType) model {
|
|
t.Helper()
|
|
|
|
next, _ := m.Update(tea.KeyMsg{Type: key})
|
|
return next.(model)
|
|
}
|
|
|
|
func TestUpdateMainMenuCursorNavigation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
|
|
m = sendKey(t, m, tea.KeyDown)
|
|
if m.cursor != 1 {
|
|
t.Fatalf("cursor=%d want 1 after down", m.cursor)
|
|
}
|
|
|
|
m = sendKey(t, m, tea.KeyDown)
|
|
if m.cursor != 2 {
|
|
t.Fatalf("cursor=%d want 2 after second down", m.cursor)
|
|
}
|
|
|
|
m = sendKey(t, m, tea.KeyUp)
|
|
if m.cursor != 1 {
|
|
t.Fatalf("cursor=%d want 1 after up", m.cursor)
|
|
}
|
|
}
|
|
|
|
func TestUpdateMainMenuEnterActions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
cursor int
|
|
wantScreen screen
|
|
wantBusy bool
|
|
wantCmd bool
|
|
}{
|
|
{name: "health_check", cursor: 0, wantScreen: screenHealthCheck, wantCmd: true},
|
|
{name: "export", cursor: 1, wantScreen: screenMain, wantBusy: true, wantCmd: true},
|
|
{name: "settings", cursor: 2, wantScreen: screenSettings, wantCmd: true},
|
|
{name: "exit", cursor: 3, wantScreen: screenMain, wantCmd: true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.cursor = test.cursor
|
|
|
|
next, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
got := next.(model)
|
|
|
|
if got.screen != test.wantScreen {
|
|
t.Fatalf("screen=%q want %q", got.screen, test.wantScreen)
|
|
}
|
|
if got.busy != test.wantBusy {
|
|
t.Fatalf("busy=%v want %v", got.busy, test.wantBusy)
|
|
}
|
|
if (cmd != nil) != test.wantCmd {
|
|
t.Fatalf("cmd present=%v want %v", cmd != nil, test.wantCmd)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateConfirmCancelViaKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenConfirm
|
|
m.pendingAction = actionRunMemorySAT
|
|
|
|
next, _ := m.Update(tea.KeyMsg{Type: tea.KeyRight})
|
|
got := next.(model)
|
|
if got.cursor != 1 {
|
|
t.Fatalf("cursor=%d want 1 after right", got.cursor)
|
|
}
|
|
|
|
next, _ = got.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
got = next.(model)
|
|
if got.screen != screenHealthCheck {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenHealthCheck)
|
|
}
|
|
if got.cursor != 0 {
|
|
t.Fatalf("cursor=%d want 0 after cancel", got.cursor)
|
|
}
|
|
}
|
|
|
|
func TestMainMenuSimpleTransitions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
cursor int
|
|
wantScreen screen
|
|
}{
|
|
{name: "health_check", cursor: 0, wantScreen: screenHealthCheck},
|
|
{name: "settings", cursor: 2, wantScreen: screenSettings},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.cursor = test.cursor
|
|
|
|
next, cmd := m.handleMainMenu()
|
|
got := next.(model)
|
|
|
|
if cmd != nil {
|
|
t.Fatalf("expected nil cmd for %s", test.name)
|
|
}
|
|
if got.screen != test.wantScreen {
|
|
t.Fatalf("screen=%q want %q", got.screen, test.wantScreen)
|
|
}
|
|
if got.cursor != 0 {
|
|
t.Fatalf("cursor=%d want 0", got.cursor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMainMenuExportSetsBusy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.cursor = 1 // Export support bundle
|
|
|
|
next, cmd := m.handleMainMenu()
|
|
got := next.(model)
|
|
|
|
if !got.busy {
|
|
t.Fatal("busy=false for export")
|
|
}
|
|
if cmd == nil {
|
|
t.Fatal("expected async cmd for export")
|
|
}
|
|
}
|
|
|
|
func TestMainViewRendersTwoColumns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.cursor = 1
|
|
|
|
view := m.View()
|
|
for _, want := range []string{
|
|
"bee",
|
|
"Health Check",
|
|
"> Export support bundle",
|
|
"Settings",
|
|
"Exit",
|
|
"│",
|
|
"[↑↓] move",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEscapeNavigation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
screen screen
|
|
wantScreen screen
|
|
}{
|
|
{name: "network to settings", screen: screenNetwork, wantScreen: screenSettings},
|
|
{name: "services to settings", screen: screenServices, wantScreen: screenSettings},
|
|
{name: "settings to main", screen: screenSettings, wantScreen: screenMain},
|
|
{name: "service action to services", screen: screenServiceAction, wantScreen: screenServices},
|
|
{name: "export targets to main", screen: screenExportTargets, wantScreen: screenMain},
|
|
{name: "interface pick to network", screen: screenInterfacePick, wantScreen: screenNetwork},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = test.screen
|
|
m.cursor = 3
|
|
|
|
next, _ := m.updateKey(tea.KeyMsg{Type: tea.KeyEsc})
|
|
got := next.(model)
|
|
|
|
if got.screen != test.wantScreen {
|
|
t.Fatalf("screen=%q want %q", got.screen, test.wantScreen)
|
|
}
|
|
if got.cursor != 0 {
|
|
t.Fatalf("cursor=%d want 0", got.cursor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckEscReturnsToMain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenHealthCheck
|
|
m.hcCursor = 3
|
|
|
|
next, _ := m.updateHealthCheck(tea.KeyMsg{Type: tea.KeyEsc})
|
|
got := next.(model)
|
|
|
|
if got.screen != screenMain {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenMain)
|
|
}
|
|
if got.cursor != 0 {
|
|
t.Fatalf("cursor=%d want 0", got.cursor)
|
|
}
|
|
}
|
|
|
|
func TestOutputScreenReturnsToPreviousScreen(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenOutput
|
|
m.prevScreen = screenNetwork
|
|
m.title = "title"
|
|
m.body = "body"
|
|
|
|
next, _ := m.updateKey(tea.KeyMsg{Type: tea.KeyEnter})
|
|
got := next.(model)
|
|
|
|
if got.screen != screenNetwork {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenNetwork)
|
|
}
|
|
if got.title != "" || got.body != "" {
|
|
t.Fatalf("expected output state cleared, got title=%q body=%q", got.title, got.body)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckGPUOpensNvidiaSATSetup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenHealthCheck
|
|
m.hcInitialized = true
|
|
m.hcSel = [4]bool{true, true, true, true}
|
|
|
|
next, cmd := m.hcRunSingle(hcGPU)
|
|
got := next.(model)
|
|
|
|
if cmd == nil {
|
|
t.Fatal("expected non-nil cmd (GPU list loader)")
|
|
}
|
|
if got.screen != screenNvidiaSATSetup {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenNvidiaSATSetup)
|
|
}
|
|
|
|
// esc from setup returns to health check
|
|
next, _ = got.updateNvidiaSATSetup(tea.KeyMsg{Type: tea.KeyEsc})
|
|
got = next.(model)
|
|
if got.screen != screenHealthCheck {
|
|
t.Fatalf("screen after esc=%q want %q", got.screen, screenHealthCheck)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckRunSingleMapsActions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
idx int
|
|
want actionKind
|
|
}{
|
|
{idx: hcMemory, want: actionRunMemorySAT},
|
|
{idx: hcStorage, want: actionRunStorageSAT},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
m := newTestModel()
|
|
m.screen = screenHealthCheck
|
|
m.hcInitialized = true
|
|
|
|
next, _ := m.hcRunSingle(test.idx)
|
|
got := next.(model)
|
|
if got.pendingAction != test.want {
|
|
t.Fatalf("idx=%d pendingAction=%q want %q", test.idx, got.pendingAction, test.want)
|
|
}
|
|
if got.screen != screenConfirm {
|
|
t.Fatalf("idx=%d screen=%q want %q", test.idx, got.screen, screenConfirm)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExportTargetSelectionOpensConfirm(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenExportTargets
|
|
m.targets = []platform.RemovableTarget{{Device: "/dev/sdb1", FSType: "vfat", Size: "16G"}}
|
|
|
|
next, cmd := m.handleExportTargetsMenu()
|
|
got := next.(model)
|
|
|
|
if cmd != nil {
|
|
t.Fatal("expected nil cmd")
|
|
}
|
|
if got.screen != screenConfirm {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenConfirm)
|
|
}
|
|
if got.pendingAction != actionExportBundle {
|
|
t.Fatalf("pendingAction=%q want %q", got.pendingAction, actionExportBundle)
|
|
}
|
|
if got.selectedTarget == nil || got.selectedTarget.Device != "/dev/sdb1" {
|
|
t.Fatalf("selectedTarget=%+v want /dev/sdb1", got.selectedTarget)
|
|
}
|
|
}
|
|
|
|
func TestInterfacePickStaticIPv4OpensForm(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.pendingAction = actionStaticIPv4
|
|
m.interfaces = []platform.InterfaceInfo{{Name: "eth0"}}
|
|
|
|
next, cmd := m.handleInterfacePickMenu()
|
|
got := next.(model)
|
|
|
|
if cmd != nil {
|
|
t.Fatal("expected nil cmd")
|
|
}
|
|
if got.screen != screenStaticForm {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenStaticForm)
|
|
}
|
|
if got.selectedIface != "eth0" {
|
|
t.Fatalf("selectedIface=%q want eth0", got.selectedIface)
|
|
}
|
|
if len(got.formFields) != 4 {
|
|
t.Fatalf("len(formFields)=%d want 4", len(got.formFields))
|
|
}
|
|
}
|
|
|
|
func TestResultMsgUsesExplicitBackScreen(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenConfirm
|
|
|
|
next, _ := m.Update(resultMsg{title: "done", body: "ok", back: screenNetwork})
|
|
got := next.(model)
|
|
|
|
if got.screen != screenOutput {
|
|
t.Fatalf("screen=%q want %q", got.screen, screenOutput)
|
|
}
|
|
if got.prevScreen != screenNetwork {
|
|
t.Fatalf("prevScreen=%q want %q", got.prevScreen, screenNetwork)
|
|
}
|
|
}
|
|
|
|
func TestConfirmCancelTarget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
|
|
m.pendingAction = actionExportBundle
|
|
if got := m.confirmCancelTarget(); got != screenExportTargets {
|
|
t.Fatalf("export cancel target=%q want %q", got, screenExportTargets)
|
|
}
|
|
|
|
m.pendingAction = actionRunAll
|
|
if got := m.confirmCancelTarget(); got != screenHealthCheck {
|
|
t.Fatalf("run all cancel target=%q want %q", got, screenHealthCheck)
|
|
}
|
|
|
|
m.pendingAction = actionRunMemorySAT
|
|
if got := m.confirmCancelTarget(); got != screenHealthCheck {
|
|
t.Fatalf("memory sat cancel target=%q want %q", got, screenHealthCheck)
|
|
}
|
|
|
|
m.pendingAction = actionRunStorageSAT
|
|
if got := m.confirmCancelTarget(); got != screenHealthCheck {
|
|
t.Fatalf("storage sat cancel target=%q want %q", got, screenHealthCheck)
|
|
}
|
|
|
|
m.pendingAction = actionNone
|
|
if got := m.confirmCancelTarget(); got != screenMain {
|
|
t.Fatalf("default cancel target=%q want %q", got, screenMain)
|
|
}
|
|
}
|
|
|
|
func TestViewBusyStateIsMinimal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.busy = true
|
|
|
|
view := m.View()
|
|
want := "bee\n\nWorking...\n\n[ctrl+c] quit\n"
|
|
if view != want {
|
|
t.Fatalf("busy view mismatch\nwant:\n%s\ngot:\n%s", want, view)
|
|
}
|
|
}
|
|
|
|
func TestViewBusyStateUsesBusyTitle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.busy = true
|
|
m.busyTitle = "Export support bundle"
|
|
|
|
view := m.View()
|
|
|
|
for _, want := range []string{
|
|
"Export support bundle",
|
|
"Working...",
|
|
"[ctrl+c] quit",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestViewOutputScreenRendersBodyAndBackHint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenOutput
|
|
m.title = "Run audit"
|
|
m.body = "audit output: /appdata/bee/export/bee-audit.json\n"
|
|
|
|
view := m.View()
|
|
|
|
for _, want := range []string{
|
|
"Run audit",
|
|
"audit output: /appdata/bee/export/bee-audit.json",
|
|
"[enter/esc] back [ctrl+c] quit",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestViewRendersBannerModuleAboveScreenBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.banner = "System: Demo Server\nIP: 10.0.0.10"
|
|
m.width = 60
|
|
|
|
view := m.View()
|
|
|
|
for _, want := range []string{
|
|
"┌ MOTD ",
|
|
"System: Demo Server",
|
|
"IP: 10.0.0.10",
|
|
"Health Check",
|
|
"Export support bundle",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSnapshotMsgUpdatesBannerAndPanel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
|
|
next, cmd := m.Update(snapshotMsg{
|
|
banner: "System: Demo",
|
|
panel: app.HardwarePanelData{
|
|
Header: []string{"Demo header"},
|
|
Rows: []app.ComponentRow{
|
|
{Key: "CPU", Status: "PASS", Detail: "ok"},
|
|
},
|
|
},
|
|
})
|
|
got := next.(model)
|
|
|
|
if cmd != nil {
|
|
t.Fatal("expected nil cmd")
|
|
}
|
|
if got.banner != "System: Demo" {
|
|
t.Fatalf("banner=%q want %q", got.banner, "System: Demo")
|
|
}
|
|
if len(got.panel.Rows) != 1 || got.panel.Rows[0].Key != "CPU" {
|
|
t.Fatalf("panel rows=%+v", got.panel.Rows)
|
|
}
|
|
}
|
|
|
|
func TestViewExportTargetsRendersDeviceMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenExportTargets
|
|
m.targets = []platform.RemovableTarget{
|
|
{
|
|
Device: "/dev/sdb1",
|
|
FSType: "vfat",
|
|
Size: "29G",
|
|
Label: "BEEUSB",
|
|
Mountpoint: "/media/bee",
|
|
},
|
|
}
|
|
|
|
view := m.View()
|
|
|
|
for _, want := range []string{
|
|
"Export support bundle",
|
|
"Select removable filesystem",
|
|
"> /dev/sdb1 [vfat 29G] label=BEEUSB mounted=/media/bee",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestViewStaticFormRendersFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenStaticForm
|
|
m.selectedIface = "enp1s0"
|
|
m.formFields = []formField{
|
|
{Label: "Address", Value: "192.0.2.10/24"},
|
|
{Label: "Gateway", Value: "192.0.2.1"},
|
|
{Label: "DNS", Value: "1.1.1.1"},
|
|
}
|
|
m.formIndex = 1
|
|
|
|
view := m.View()
|
|
|
|
for _, want := range []string{
|
|
"Static IPv4: enp1s0",
|
|
" Address: 192.0.2.10/24",
|
|
"> Gateway: 192.0.2.1",
|
|
" DNS: 1.1.1.1",
|
|
"[tab/↑/↓] move [enter] next/submit [backspace] delete [esc] cancel",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestViewConfirmScreenMatchesPendingExport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.screen = screenConfirm
|
|
m.pendingAction = actionExportBundle
|
|
m.selectedTarget = &platform.RemovableTarget{Device: "/dev/sdb1"}
|
|
|
|
view := m.View()
|
|
|
|
for _, want := range []string{
|
|
"Export support bundle",
|
|
"Copy support bundle to /dev/sdb1?",
|
|
"> Confirm",
|
|
" Cancel",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResultMsgClearsBusyAndPendingAction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
m.busy = true
|
|
m.busyTitle = "Export support bundle"
|
|
m.pendingAction = actionExportBundle
|
|
m.screen = screenConfirm
|
|
|
|
next, _ := m.Update(resultMsg{title: "Export support bundle", body: "done", back: screenMain})
|
|
got := next.(model)
|
|
|
|
if got.busy {
|
|
t.Fatal("busy=true want false")
|
|
}
|
|
if got.busyTitle != "" {
|
|
t.Fatalf("busyTitle=%q want empty", got.busyTitle)
|
|
}
|
|
if got.pendingAction != actionNone {
|
|
t.Fatalf("pendingAction=%q want empty", got.pendingAction)
|
|
}
|
|
}
|
|
|
|
func TestResultMsgErrorWithoutBodyFormatsCleanly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
m := newTestModel()
|
|
|
|
next, _ := m.Update(resultMsg{title: "Export support bundle", err: assertErr("boom"), back: screenMain})
|
|
got := next.(model)
|
|
|
|
if got.body != "ERROR: boom" {
|
|
t.Fatalf("body=%q want %q", got.body, "ERROR: boom")
|
|
}
|
|
}
|
|
|
|
type assertErr string
|
|
|
|
func (e assertErr) Error() string { return string(e) }
|