Skip to content

Commit ff70ea6

Browse files
committed
fixes
1 parent 90b297d commit ff70ea6

18 files changed

+1992
-101
lines changed

cmd/push-validator/cmd_backup.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
package main
22

33
import (
4-
"fmt"
4+
"fmt"
55

6-
"github.com/pushchain/push-validator-cli/internal/admin"
6+
"github.com/pushchain/push-validator-cli/internal/admin"
77
)
88

99
// handleBackup creates a backup archive of the node configuration and
1010
// prints the resulting path, or a JSON object when --output=json.
1111
func handleBackup(d *Deps) error {
12-
path, err := admin.Backup(admin.BackupOptions{HomeDir: d.Cfg.HomeDir})
13-
if err != nil {
14-
if flagOutput == "json" { d.Printer.JSON(map[string]any{"ok": false, "error": err.Error()}) } else { d.Printer.Error(fmt.Sprintf("backup error: %v", err)) }
15-
return err
16-
}
17-
if flagOutput == "json" { d.Printer.JSON(map[string]any{"ok": true, "backup_path": path}) } else { d.Printer.Success(fmt.Sprintf("backup created: %s", path)) }
18-
return nil
12+
return handleBackupWith(d, func(opts admin.BackupOptions) (string, error) {
13+
return admin.Backup(opts)
14+
})
15+
}
16+
17+
// handleBackupWith is the testable core of handleBackup with an injectable backup function.
18+
func handleBackupWith(d *Deps, backupFn func(admin.BackupOptions) (string, error)) error {
19+
path, err := backupFn(admin.BackupOptions{HomeDir: d.Cfg.HomeDir})
20+
if err != nil {
21+
if flagOutput == "json" {
22+
d.Printer.JSON(map[string]any{"ok": false, "error": err.Error()})
23+
} else {
24+
d.Printer.Error(fmt.Sprintf("backup error: %v", err))
25+
}
26+
return err
27+
}
28+
if flagOutput == "json" {
29+
d.Printer.JSON(map[string]any{"ok": true, "backup_path": path})
30+
} else {
31+
d.Printer.Success(fmt.Sprintf("backup created: %s", path))
32+
}
33+
return nil
1934
}

cmd/push-validator/cmd_backup_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package main
22

33
import (
4+
"fmt"
45
"testing"
6+
7+
"github.com/pushchain/push-validator-cli/internal/admin"
58
)
69

710
func TestHandleBackup_Success(t *testing.T) {
@@ -111,3 +114,118 @@ func TestHandleBackup_Success_Text(t *testing.T) {
111114
t.Fatalf("unexpected error: %v", err)
112115
}
113116
}
117+
118+
// --- Tests using handleBackupWith with injectable backup function ---
119+
120+
func TestHandleBackupWith_Success_JSON(t *testing.T) {
121+
origOutput := flagOutput
122+
defer func() { flagOutput = origOutput }()
123+
flagOutput = "json"
124+
125+
d := &Deps{
126+
Cfg: testCfg(),
127+
Printer: getPrinter(),
128+
}
129+
130+
err := handleBackupWith(d, func(opts admin.BackupOptions) (string, error) {
131+
return "/tmp/backup.tar.gz", nil
132+
})
133+
if err != nil {
134+
t.Fatalf("unexpected error: %v", err)
135+
}
136+
}
137+
138+
func TestHandleBackupWith_Success_Text(t *testing.T) {
139+
origOutput := flagOutput
140+
origNoColor := flagNoColor
141+
origNoEmoji := flagNoEmoji
142+
defer func() {
143+
flagOutput = origOutput
144+
flagNoColor = origNoColor
145+
flagNoEmoji = origNoEmoji
146+
}()
147+
flagOutput = "text"
148+
flagNoColor = true
149+
flagNoEmoji = true
150+
151+
d := &Deps{
152+
Cfg: testCfg(),
153+
Printer: getPrinter(),
154+
}
155+
156+
err := handleBackupWith(d, func(opts admin.BackupOptions) (string, error) {
157+
return "/tmp/backup.tar.gz", nil
158+
})
159+
if err != nil {
160+
t.Fatalf("unexpected error: %v", err)
161+
}
162+
}
163+
164+
func TestHandleBackupWith_Error_JSON(t *testing.T) {
165+
origOutput := flagOutput
166+
defer func() { flagOutput = origOutput }()
167+
flagOutput = "json"
168+
169+
d := &Deps{
170+
Cfg: testCfg(),
171+
Printer: getPrinter(),
172+
}
173+
174+
err := handleBackupWith(d, func(opts admin.BackupOptions) (string, error) {
175+
return "", fmt.Errorf("disk full")
176+
})
177+
if err == nil || err.Error() != "disk full" {
178+
t.Errorf("expected 'disk full', got: %v", err)
179+
}
180+
}
181+
182+
func TestHandleBackupWith_Error_Text(t *testing.T) {
183+
origOutput := flagOutput
184+
origNoColor := flagNoColor
185+
origNoEmoji := flagNoEmoji
186+
defer func() {
187+
flagOutput = origOutput
188+
flagNoColor = origNoColor
189+
flagNoEmoji = origNoEmoji
190+
}()
191+
flagOutput = "text"
192+
flagNoColor = true
193+
flagNoEmoji = true
194+
195+
d := &Deps{
196+
Cfg: testCfg(),
197+
Printer: getPrinter(),
198+
}
199+
200+
err := handleBackupWith(d, func(opts admin.BackupOptions) (string, error) {
201+
return "", fmt.Errorf("permission denied")
202+
})
203+
if err == nil || err.Error() != "permission denied" {
204+
t.Errorf("expected 'permission denied', got: %v", err)
205+
}
206+
}
207+
208+
func TestHandleBackupWith_VerifiesHomeDir(t *testing.T) {
209+
origOutput := flagOutput
210+
defer func() { flagOutput = origOutput }()
211+
flagOutput = "json"
212+
213+
cfg := testCfg()
214+
cfg.HomeDir = "/custom/home"
215+
d := &Deps{
216+
Cfg: cfg,
217+
Printer: getPrinter(),
218+
}
219+
220+
var capturedOpts admin.BackupOptions
221+
err := handleBackupWith(d, func(opts admin.BackupOptions) (string, error) {
222+
capturedOpts = opts
223+
return "/backup.tar.gz", nil
224+
})
225+
if err != nil {
226+
t.Fatalf("unexpected error: %v", err)
227+
}
228+
if capturedOpts.HomeDir != "/custom/home" {
229+
t.Errorf("expected HomeDir=/custom/home, got %s", capturedOpts.HomeDir)
230+
}
231+
}

cmd/push-validator/cmd_dashboard.go

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ import (
1414
"github.com/pushchain/push-validator-cli/internal/dashboard"
1515
)
1616

17+
// dashboardFlags holds the parsed flag values for the dashboard command.
18+
type dashboardFlags struct {
19+
refreshInterval time.Duration
20+
rpcTimeout time.Duration
21+
debugMode bool
22+
}
23+
24+
// dashboardCoreDeps holds injectable dependencies for runDashboardCmdCore.
25+
type dashboardCoreDeps struct {
26+
isTTY func() bool
27+
runStatic func(ctx context.Context, opts dashboard.Options) error
28+
runInteractive func(opts dashboard.Options) error
29+
}
30+
31+
// runDashboardCmdCore contains the testable logic for the dashboard RunE handler.
32+
func runDashboardCmdCore(ctx context.Context, opts dashboard.Options, deps dashboardCoreDeps) error {
33+
if !deps.isTTY() {
34+
if opts.Debug {
35+
fmt.Fprintln(os.Stderr, "Debug: Non-TTY detected, using static mode")
36+
}
37+
return deps.runStatic(ctx, opts)
38+
}
39+
40+
if opts.Debug {
41+
fmt.Fprintln(os.Stderr, "Debug: TTY detected, using interactive mode")
42+
}
43+
return deps.runInteractive(opts)
44+
}
45+
1746
// dashboardCmd provides an interactive TUI dashboard for monitoring validator status
1847
func createDashboardCmd() *cobra.Command {
1948
var (
@@ -37,10 +66,7 @@ The dashboard auto-refreshes every 2 seconds by default. Press '?' for help.
3766
For non-interactive environments (CI/pipes), dashboard automatically falls back
3867
to a static text snapshot.`,
3968
RunE: func(cmd *cobra.Command, args []string) error {
40-
// Load configuration
4169
cfg := loadCfg()
42-
43-
// Build dashboard options
4470
opts := dashboard.Options{
4571
Config: cfg,
4672
RefreshInterval: refreshInterval,
@@ -52,26 +78,14 @@ to a static text snapshot.`,
5278
}
5379
opts = normalizeDashboardOptions(opts)
5480

55-
// Check if we're in a TTY environment
56-
isTTY := term.IsTerminal(int(os.Stdout.Fd()))
57-
58-
if !isTTY {
59-
// Non-TTY environment (CI/pipes): use static mode
60-
if debugMode {
61-
fmt.Fprintln(os.Stderr, "Debug: Non-TTY detected, using static mode")
62-
}
63-
return runDashboardStatic(cmd.Context(), opts)
64-
}
65-
66-
// TTY environment: use interactive Bubble Tea dashboard
67-
if debugMode {
68-
fmt.Fprintln(os.Stderr, "Debug: TTY detected, using interactive mode")
69-
}
70-
return runDashboardInteractive(opts)
81+
return runDashboardCmdCore(cmd.Context(), opts, dashboardCoreDeps{
82+
isTTY: func() bool { return term.IsTerminal(int(os.Stdout.Fd())) },
83+
runStatic: runDashboardStatic,
84+
runInteractive: runDashboardInteractive,
85+
})
7186
},
7287
}
7388

74-
// Flags
7589
cmd.Flags().DurationVar(&refreshInterval, "refresh-interval", 2*time.Second, "Dashboard refresh interval")
7690
cmd.Flags().DurationVar(&rpcTimeout, "rpc-timeout", 15*time.Second, "RPC request timeout")
7791
cmd.Flags().BoolVar(&debugMode, "debug", false, "Enable debug mode for troubleshooting")

cmd/push-validator/cmd_dashboard_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
"fmt"
46
"testing"
57
"time"
68

@@ -78,3 +80,127 @@ func TestNormalizeDashboardOptions_SmallRefresh(t *testing.T) {
7880
t.Errorf("RPCTimeout = %v, want 2s", opts.RPCTimeout)
7981
}
8082
}
83+
84+
// --- Tests for runDashboardCmdCore ---
85+
86+
func TestRunDashboardCmdCore_NonTTY_CallsStatic(t *testing.T) {
87+
staticCalled := false
88+
interactiveCalled := false
89+
90+
deps := dashboardCoreDeps{
91+
isTTY: func() bool { return false },
92+
runStatic: func(ctx context.Context, opts dashboard.Options) error {
93+
staticCalled = true
94+
return nil
95+
},
96+
runInteractive: func(opts dashboard.Options) error {
97+
interactiveCalled = true
98+
return nil
99+
},
100+
}
101+
102+
opts := dashboard.Options{RefreshInterval: 2 * time.Second}
103+
err := runDashboardCmdCore(context.Background(), opts, deps)
104+
if err != nil {
105+
t.Fatalf("unexpected error: %v", err)
106+
}
107+
if !staticCalled {
108+
t.Error("expected runStatic to be called")
109+
}
110+
if interactiveCalled {
111+
t.Error("expected runInteractive NOT to be called")
112+
}
113+
}
114+
115+
func TestRunDashboardCmdCore_TTY_CallsInteractive(t *testing.T) {
116+
staticCalled := false
117+
interactiveCalled := false
118+
119+
deps := dashboardCoreDeps{
120+
isTTY: func() bool { return true },
121+
runStatic: func(ctx context.Context, opts dashboard.Options) error {
122+
staticCalled = true
123+
return nil
124+
},
125+
runInteractive: func(opts dashboard.Options) error {
126+
interactiveCalled = true
127+
return nil
128+
},
129+
}
130+
131+
opts := dashboard.Options{RefreshInterval: 2 * time.Second}
132+
err := runDashboardCmdCore(context.Background(), opts, deps)
133+
if err != nil {
134+
t.Fatalf("unexpected error: %v", err)
135+
}
136+
if staticCalled {
137+
t.Error("expected runStatic NOT to be called")
138+
}
139+
if !interactiveCalled {
140+
t.Error("expected runInteractive to be called")
141+
}
142+
}
143+
144+
func TestRunDashboardCmdCore_Static_Error(t *testing.T) {
145+
deps := dashboardCoreDeps{
146+
isTTY: func() bool { return false },
147+
runStatic: func(ctx context.Context, opts dashboard.Options) error {
148+
return fmt.Errorf("fetch failed")
149+
},
150+
runInteractive: func(opts dashboard.Options) error { return nil },
151+
}
152+
153+
opts := dashboard.Options{}
154+
err := runDashboardCmdCore(context.Background(), opts, deps)
155+
if err == nil || err.Error() != "fetch failed" {
156+
t.Errorf("expected 'fetch failed', got: %v", err)
157+
}
158+
}
159+
160+
func TestRunDashboardCmdCore_Interactive_Error(t *testing.T) {
161+
deps := dashboardCoreDeps{
162+
isTTY: func() bool { return true },
163+
runStatic: func(ctx context.Context, opts dashboard.Options) error { return nil },
164+
runInteractive: func(opts dashboard.Options) error {
165+
return fmt.Errorf("TUI error")
166+
},
167+
}
168+
169+
opts := dashboard.Options{}
170+
err := runDashboardCmdCore(context.Background(), opts, deps)
171+
if err == nil || err.Error() != "TUI error" {
172+
t.Errorf("expected 'TUI error', got: %v", err)
173+
}
174+
}
175+
176+
func TestRunDashboardCmdCore_Debug_NonTTY(t *testing.T) {
177+
deps := dashboardCoreDeps{
178+
isTTY: func() bool { return false },
179+
runStatic: func(ctx context.Context, opts dashboard.Options) error {
180+
return nil
181+
},
182+
runInteractive: func(opts dashboard.Options) error { return nil },
183+
}
184+
185+
opts := dashboard.Options{Debug: true}
186+
err := runDashboardCmdCore(context.Background(), opts, deps)
187+
if err != nil {
188+
t.Fatalf("unexpected error: %v", err)
189+
}
190+
}
191+
192+
func TestRunDashboardCmdCore_Debug_TTY(t *testing.T) {
193+
deps := dashboardCoreDeps{
194+
isTTY: func() bool { return true },
195+
runStatic: func(ctx context.Context, opts dashboard.Options) error { return nil },
196+
runInteractive: func(opts dashboard.Options) error {
197+
return nil
198+
},
199+
}
200+
201+
opts := dashboard.Options{Debug: true}
202+
err := runDashboardCmdCore(context.Background(), opts, deps)
203+
if err != nil {
204+
t.Fatalf("unexpected error: %v", err)
205+
}
206+
}

0 commit comments

Comments
 (0)