package sshutilquality import ( "bytes" "context" "errors" "log" "os" "path/filepath" "strings" "testing" "scm.bstein.dev/bstein/ananke/internal/sshutil" ) // TestHostKeyHeuristics runs one orchestration or CLI step. // Signature: TestHostKeyHeuristics(t *testing.T). // Why: covers host-key error detection and repair-decision heuristics used by bootstrap SSH retries. func TestHostKeyHeuristics(t *testing.T) { if sshutil.IsHostKeyError("", nil) { t.Fatalf("expected nil error to not be host-key related") } if !sshutil.IsHostKeyError("Host key verification failed", errors.New("ssh failure")) { t.Fatalf("expected host-key verification text to be detected") } if !sshutil.ShouldAttemptKnownHostsRepair("", errors.New("exit status 255")) { t.Fatalf("expected empty-output exit-255 branch to trigger known_hosts repair") } if sshutil.ShouldAttemptKnownHostsRepair("permission denied", errors.New("permission denied")) { t.Fatalf("unexpected known_hosts repair suggestion for non host-key error") } } // TestKnownHostsDiscoveryAndMissingBinaryBranch runs one orchestration or CLI step. // Signature: TestKnownHostsDiscoveryAndMissingBinaryBranch(t *testing.T). // Why: verifies known_hosts path discovery and the graceful no-op path when ssh-keygen is unavailable. func TestKnownHostsDiscoveryAndMissingBinaryBranch(t *testing.T) { dir := t.TempDir() cfgPath := filepath.Join(dir, "config") keyPath := filepath.Join(dir, "id_ed25519") if err := os.WriteFile(cfgPath, []byte("Host *\n"), 0o600); err != nil { t.Fatalf("write config: %v", err) } if err := os.WriteFile(keyPath, []byte("key"), 0o600); err != nil { t.Fatalf("write identity: %v", err) } files := sshutil.KnownHostsFiles(cfgPath, keyPath) foundLocal := false for _, path := range files { if strings.Contains(path, dir) { foundLocal = true break } } if !foundLocal { t.Fatalf("expected derived known_hosts entries under %s, got %v", dir, files) } originalPath := os.Getenv("PATH") t.Setenv("PATH", filepath.Join(dir, "missing-bin")) var logs bytes.Buffer sshutil.RepairKnownHosts(context.Background(), log.New(&logs, "", 0), files, []string{"titan-db"}, 22) t.Setenv("PATH", originalPath) if !strings.Contains(logs.String(), "ssh-keygen missing") { t.Fatalf("expected missing ssh-keygen warning, got: %s", logs.String()) } } // TestRepairKnownHostsCoversSuccessNotFoundAndFailure runs one orchestration or CLI step. // Signature: TestRepairKnownHostsCoversSuccessNotFoundAndFailure(t *testing.T). // Why: exercises cleanup outcomes for successful removal, absent entries, and hard failures. func TestRepairKnownHostsCoversSuccessNotFoundAndFailure(t *testing.T) { dir := t.TempDir() scriptPath := filepath.Join(dir, "ssh-keygen") script := `#!/usr/bin/env sh args="$*" if echo "$args" | grep -q "notfound"; then echo "Host notfound not found in $args" >&2 exit 1 fi if echo "$args" | grep -q "boom"; then echo "boom failure" >&2 exit 2 fi echo "updated" >&2 exit 0 ` if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil { t.Fatalf("write fake ssh-keygen: %v", err) } t.Setenv("PATH", dir+":"+os.Getenv("PATH")) knownHosts := filepath.Join(dir, "known_hosts") if err := os.WriteFile(knownHosts, []byte("example"), 0o644); err != nil { t.Fatalf("write known_hosts: %v", err) } if err := os.WriteFile(knownHosts+".old", []byte("backup"), 0o644); err != nil { t.Fatalf("write known_hosts backup: %v", err) } var logs bytes.Buffer logger := log.New(&logs, "", 0) sshutil.RepairKnownHosts( context.Background(), logger, []string{knownHosts, knownHosts}, []string{"good", "notfound", "boom", "good"}, 22, ) out := logs.String() if !strings.Contains(out, "known_hosts repaired: removed good") { t.Fatalf("expected successful cleanup log, got: %s", out) } if !strings.Contains(out, "warning: known_hosts cleanup failed for boom") { t.Fatalf("expected failure warning for boom host, got: %s", out) } }