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: "network", cursor: 0, wantScreen: screenNetwork}, {name: "services", cursor: 1, wantScreen: screenMain, wantBusy: true, wantCmd: true}, {name: "acceptance", cursor: 2, wantScreen: screenAcceptance}, {name: "run audit", cursor: 3, wantScreen: screenMain, wantBusy: true, wantCmd: true}, {name: "export", cursor: 4, wantScreen: screenMain, wantBusy: true, 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 = actionRunNvidiaSAT 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 != screenAcceptance { t.Fatalf("screen=%q want %q", got.screen, screenAcceptance) } 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: "network", cursor: 0, wantScreen: screenNetwork}, {name: "acceptance", cursor: 2, wantScreen: screenAcceptance}, } 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 TestMainMenuAsyncActionsSetBusy(t *testing.T) { t.Parallel() tests := []struct { name string cursor int }{ {name: "services", cursor: 1}, {name: "run audit", cursor: 3}, {name: "export", cursor: 4}, {name: "check tools", cursor: 5}, {name: "health summary", cursor: 6}, {name: "log tail", cursor: 7}, } 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 !got.busy { t.Fatalf("busy=false for %s", test.name) } if cmd == nil { t.Fatalf("expected async cmd for %s", test.name) } }) } } func TestMainViewIncludesBanner(t *testing.T) { t.Parallel() m := newTestModel() m.banner = "System: Test Server | S/N ABC123\nIP: 10.0.0.10" view := m.View() if !strings.Contains(view, "System: Test Server | S/N ABC123") { t.Fatalf("view missing system banner:\n%s", view) } if !strings.Contains(view, "IP: 10.0.0.10") { t.Fatalf("view missing ip banner:\n%s", view) } if !strings.Contains(view, "Select action") { t.Fatalf("view missing menu subtitle:\n%s", view) } } func TestEscapeNavigation(t *testing.T) { t.Parallel() tests := []struct { name string screen screen wantScreen screen }{ {name: "network to main", screen: screenNetwork, wantScreen: screenMain}, {name: "services to main", screen: screenServices, wantScreen: screenMain}, {name: "acceptance to main", screen: screenAcceptance, 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 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 TestAcceptanceConfirmFlow(t *testing.T) { t.Parallel() m := newTestModel() m.screen = screenAcceptance m.cursor = 0 next, cmd := m.handleAcceptanceMenu() 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 != actionRunNvidiaSAT { t.Fatalf("pendingAction=%q want %q", got.pendingAction, actionRunNvidiaSAT) } next, _ = got.updateConfirm(tea.KeyMsg{Type: tea.KeyEsc}) got = next.(model) if got.screen != screenAcceptance { t.Fatalf("screen after esc=%q want %q", got.screen, screenAcceptance) } } func TestAcceptanceMenuMapsNewTargets(t *testing.T) { t.Parallel() tests := []struct { cursor int want actionKind }{ {cursor: 0, want: actionRunNvidiaSAT}, {cursor: 1, want: actionRunMemorySAT}, {cursor: 2, want: actionRunStorageSAT}, } for _, test := range tests { m := newTestModel() m.screen = screenAcceptance m.cursor = test.cursor next, _ := m.handleAcceptanceMenu() got := next.(model) if got.pendingAction != test.want { t.Fatalf("cursor=%d pendingAction=%q want %q", test.cursor, got.pendingAction, test.want) } } } 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 != actionExportAudit { t.Fatalf("pendingAction=%q want %q", got.pendingAction, actionExportAudit) } 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 = actionExportAudit if got := m.confirmCancelTarget(); got != screenExportTargets { t.Fatalf("export cancel target=%q want %q", got, screenExportTargets) } m.pendingAction = actionRunNvidiaSAT if got := m.confirmCancelTarget(); got != screenAcceptance { t.Fatalf("sat cancel target=%q want %q", got, screenAcceptance) } m.pendingAction = actionNone if got := m.confirmCancelTarget(); got != screenMain { t.Fatalf("default cancel target=%q want %q", got, screenMain) } } func TestViewMainMenuRendersSelectedItem(t *testing.T) { t.Parallel() m := newTestModel() m.cursor = 1 view := m.View() for _, want := range []string{ "bee", "Select action", " Network", "> Services", "Acceptance tests", "[↑/↓] move [enter] select [esc] back [ctrl+c] quit", } { if !strings.Contains(view, want) { t.Fatalf("view missing %q\nview:\n%s", want, view) } } } 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 audit" view := m.View() for _, want := range []string{ "Export audit", "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: /var/log/bee-audit.json\n" view := m.View() for _, want := range []string{ "Run audit", "audit output: /var/log/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 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 audit", "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 = actionExportAudit m.selectedTarget = &platform.RemovableTarget{Device: "/dev/sdb1"} view := m.View() for _, want := range []string{ "Export audit", "Copy latest audit JSON 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 audit" m.pendingAction = actionExportAudit m.screen = screenConfirm next, _ := m.Update(resultMsg{title: "Export audit", 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 audit", 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) }