Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ type Model struct {
vals formValues // the running scan's settings, for its report
seed uint64
started time.Time

// outputErr records that the results file could not be written, so the
// process exits non-zero even when the scan itself succeeded (#155).
outputErr bool
}

// Options configure the TUI.
Expand All @@ -65,8 +69,9 @@ func New(opts Options) Model {
}
}

// Start runs the TUI and returns an exit code: 1 if the TUI itself failed or
// the last scan failed (#55), else 0.
// Start runs the TUI and returns an exit code: 1 if the TUI itself failed,
// the last scan failed (#55), or the results file could not be written (#155),
// else 0.
func Start(opts Options) int {
p := tea.NewProgram(New(opts), tea.WithAltScreen())
final, err := p.Run()
Expand All @@ -78,7 +83,7 @@ func Start(opts Options) int {

// exitCode maps the final model to the process exit code.
func exitCode(final tea.Model) int {
if m, ok := final.(Model); ok && m.scanView.failed() {
if m, ok := final.(Model); ok && (m.scanView.failed() || m.outputErr) {
return 1
}
return 0
Expand Down Expand Up @@ -331,9 +336,11 @@ func (m *Model) summary(done doneMsg) output.Summary {
return s
}

// reportOutputErr surfaces an output-file failure in the scan view.
// reportOutputErr surfaces an output-file failure in the scan view and makes
// it fail the run, as it does on the command line (#155).
func (m *Model) reportOutputErr(err error) {
if err != nil {
m.outputErr = true
m.scanView.addMessage(errorStyle.Render("✗ output file incomplete: " + err.Error()))
}
}
Expand Down
29 changes: 29 additions & 0 deletions internal/tui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,35 @@ func TestFinalizeOutputReportsCloseError(t *testing.T) {
}
}

// TestOutputFileFailureSetsExitCode covers #155: when the results file cannot
// be written, the TUI exits 1 like the CLI does, even though the scan itself
// succeeded.
func TestOutputFileFailureSetsExitCode(t *testing.T) {
path := filepath.Join(t.TempDir(), "out.txt")
f, err := output.CreateFile(path)
if err != nil {
t.Fatal(err)
}
m := Model{out: output.NewFile(f.Writer, false, output.FormatText), outFile: f}
m.out.Result("a.example.com", nil)
// Put a non-empty directory where the results file goes, so the final
// rename into place fails.
if err := os.MkdirAll(filepath.Join(path, "blocker"), 0o755); err != nil {
t.Fatal(err)
}
m.reportOutputErr(m.finalizeOutput(true))
if code := exitCode(m); code != 1 {
t.Fatalf("exit code = %d, want 1 after an output-file failure", code)
}
if !strings.Contains(m.scanView.View(), "output file incomplete") {
t.Errorf("scan view does not report the incomplete results file: %s", m.scanView.View())
}
// A run without an output-file failure still exits 0.
if code := exitCode(Model{}); code != 0 {
t.Errorf("exit code = %d, want 0 without an output-file failure", code)
}
}

// TestValidateRejectsStdinWordlist covers #55: "-" would read the stdin that
// Bubble Tea owns and freeze the UI.
func TestValidateRejectsStdinWordlist(t *testing.T) {
Expand Down