package cli import ( "bytes" "context" "encoding/json" "strings" "testing" "time" "git.cloudinit.dev/coreci/orca/internal/certpaths" "git.cloudinit.dev/coreci/orca/internal/store" ) func TestDoctorText(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor: %v", err) } out := buf.String() for _, want := range []string{"CA", "cert", "PASS", "WARN", "FAIL"} { _ = want } if !strings.Contains(out, "CA") { t.Errorf("doctor output missing CA check: %s", out) } } func TestDoctorJSON(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor --json: %v", err) } var checks []map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &checks); err != nil { t.Fatalf("unmarshal doctor json: %v\n%s", err, buf.String()) } if len(checks) == 0 { t.Errorf("doctor --json returned no checks: %s", buf.String()) } } func TestDoctorCertSubcommand(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "cert"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor cert: %v", err) } out := buf.String() if !strings.Contains(out, "CA") { t.Errorf("doctor cert output missing CA: %s", out) } } func TestDoctorCertJSON(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "cert", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor cert --json: %v", err) } var results []map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &results); err != nil { t.Fatalf("unmarshal doctor cert json: %v\n%s", err, buf.String()) } if len(results) == 0 { t.Errorf("doctor cert --json returned no results: %s", buf.String()) } } func TestDoctorDBSubcommand(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "db"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor db: %v", err) } out := buf.String() if !strings.Contains(out, "db") { t.Errorf("doctor db output unexpected: %s", out) } } func TestDoctorOSSubcommand(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "os"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor os: %v", err) } } func TestDoctorOSJSON(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "os", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor os --json: %v", err) } var result map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil { t.Fatalf("unmarshal doctor os json: %v\n%s", err, buf.String()) } if result["Name"] == nil { t.Errorf("doctor os --json missing Name: %v", result) } } func TestDoctorNetworkSubcommand(t *testing.T) { resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "network"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor network: %v", err) } } func TestDoctorProxmoxSubcommand(t *testing.T) { resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "proxmox"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor proxmox: %v", err) } } func TestDoctorProxmoxJSON(t *testing.T) { resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "proxmox", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor proxmox --json: %v", err) } var result map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil { t.Fatalf("unmarshal doctor proxmox json: %v\n%s", err, buf.String()) } if result["Name"] == nil { t.Errorf("doctor proxmox --json missing Name: %v", result) } } // TestDoctorDBRetention verifies that `orca doctor db-retention` counts // rows in jobs, tasks, and audit_log and warns when a table exceeds // 100k rows (REQ-158, P09 T7). func TestDoctorDBRetention(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } // Insert 100001 rows into the audit_log table to trigger the warning. // Use a multi-row VALUES insert in batches for speed. db, err := store.Open(certpaths.DBPath()) if err != nil { t.Fatalf("open db: %v", err) } defer db.Close() ctx := context.Background() // Build a batch insert: 500 rows per INSERT in a transaction. // SQLite handles this much faster than 100k individual inserts. const totalRows = 100001 const batchSize = 500 inserted := 0 for inserted < totalRows { remaining := totalRows - inserted batch := batchSize if remaining < batch { batch = remaining } var placeholders strings.Builder var args []any for j := 0; j < batch; j++ { if j > 0 { placeholders.WriteString(",") } placeholders.WriteString("(?, 'test', 'test.action', 'test-resource', 'success')") args = append(args, time.Now().UTC()) } q := "INSERT INTO audit_log (timestamp, actor, action, resource, result) VALUES " + placeholders.String() if _, err := db.ExecContext(ctx, q, args...); err != nil { t.Fatalf("batch insert at offset %d: %v", inserted, err) } inserted += batch } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "db-retention"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor db-retention: %v", err) } out := buf.String() if !strings.Contains(out, "audit_log") { t.Errorf("output missing audit_log table: %s", out) } if !strings.Contains(out, "WARN") { t.Errorf("output should contain WARN for audit_log exceeding threshold: %s", out) } if !strings.Contains(out, "backup") { t.Errorf("output should suggest 'orca backup': %s", out) } } // TestDoctorDBRetentionNoWarn verifies that with a small DB no warning // is emitted (REQ-158, P09 T7). func TestDoctorDBRetentionNoWarn(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"doctor", "db-retention"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("doctor db-retention: %v", err) } out := buf.String() if strings.Contains(out, "WARN") { t.Errorf("output should NOT contain WARN for small DB: %s", out) } }