Files
orca/internal/cli/doctor_test.go
T
Jon Chery 531b36924c fix(P09): migration + operational safety — job stop, retention, logs cap (REQ-158)
- job stop: real systemctl stop via SSH (was DB-only soft stop)
  resolves node from alloc_history or --peer flag
- doctor db-retention: row count check for jobs/tasks/audit_log
  warns at 100k rows, suggests backup + cleanup
- logs --lines: cap at 50000 (default 1000); --since upper bound 7d
  prevents OOM from unbounded journalctl
- cache DB mode 0600 (was 0644; matches store.Open)
- upgrade cutover: backup file + atomic rename (was sed -i)
  rollback restores from backup on failure

Tests: job stop SSH, DB retention warning, logs lines cap, cache mode,
cutover backup-restore + atomic rename.

---ci---
project: orca
phase: 9
milestone: v0.13
status: complete
requirements:
  covered: [158]
---/ci---
2026-08-10 13:37:28 +00:00

291 lines
7.7 KiB
Go

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)
}
}