6408342a7f
Add table-driven rootCmd.Execute() tests for the node, job, cert, doctor, audit, status, version, and node-capacity subcommand families. Each test runs against a temp ORCA_HOME and asserts stdout/stderr/exit via the existing initTestEnv/resetRootFlags/discardWriter helpers (RESEARCH §1.2). extend resetRootFlags to also reset the per-command flag-bound globals so tests don't leak state between runs. daemon.go is excluded from the ≥70% target (documented in node_test.go): the daemon command starts a long-running mTLS server whose lifecycle is covered by internal/daemon/server_test.go; only its --pprof flag registration is verified here (daemon_test.go). Coverage: go test -cover ./internal/cli → 76.2% overall (78.7% by -func), which includes daemon.go's untested RunE; the non-daemon files exceed 70% comfortably. go test -race PASS. ---ci--- project: orca phase: 1 milestone: v0.8 status: execute ---/ci---
321 lines
8.9 KiB
Go
321 lines
8.9 KiB
Go
// This file tests the `orca node` subcommand family (join/leave/list,
|
|
// capacity is covered in node_capacity_test.go). Tests execute rootCmd
|
|
// against a temp ORCA_HOME and assert stdout/stderr/exit per RESEARCH
|
|
// §1.2.
|
|
//
|
|
// daemon.go is EXCLUDED from the cli ≥70% coverage target: the daemon
|
|
// command starts a long-running mTLS server whose lifecycle is better
|
|
// covered by internal/daemon/server_test.go (already 150 LOC). The
|
|
// --pprof flag registration is verified in daemon_test.go.
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
|
"git.cloudinit.dev/coreci/orca/internal/model"
|
|
"git.cloudinit.dev/coreci/orca/internal/security"
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
func TestNodeJoinLocalText(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "worker-1", "--addr", "10.0.0.5:8443"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node join: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "Node joined") {
|
|
t.Errorf("node join output unexpected: %s", out)
|
|
}
|
|
if !strings.Contains(out, "worker-1") {
|
|
t.Errorf("node join output missing name: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinLocalJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "worker-2", "--addr", "10.0.0.6:8443", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node join --json: %v", err)
|
|
}
|
|
var node map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &node); err != nil {
|
|
t.Fatalf("unmarshal node json: %v\n%s", err, buf.String())
|
|
}
|
|
if node["name"] != "worker-2" {
|
|
t.Errorf("node join --json name = %v, want worker-2", node["name"])
|
|
}
|
|
if node["address"] != "10.0.0.6:8443" {
|
|
t.Errorf("node join --json address = %v, want 10.0.0.6:8443", node["address"])
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinMissingName(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for missing --name, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinDefaultAddr(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "defaulter", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node join: %v", err)
|
|
}
|
|
var node map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &node); err != nil {
|
|
t.Fatalf("unmarshal node json: %v\n%s", err, buf.String())
|
|
}
|
|
if node["address"] != "localhost:8443" {
|
|
t.Errorf("node join default addr = %v, want localhost:8443", node["address"])
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinCAFingerprintMatch(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
if err := runInit(discardWriter{}); err != nil {
|
|
t.Fatalf("init: %v", err)
|
|
}
|
|
fp, err := security.Fingerprint(certpaths.CACertPath())
|
|
if err != nil {
|
|
t.Fatalf("fingerprint: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "pinned", "--ca-fingerprint", fp, "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node join with matching fingerprint: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinCAFingerprintMismatch(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "badpin", "--ca-fingerprint", padHex(64)})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for CA fingerprint mismatch, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinCAFingerprintNoCA(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "noca", "--ca-fingerprint", padHex(64)})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for missing CA with --ca-fingerprint, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinProxmoxMissingHost(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--type", "proxmox", "--password", "x"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for proxmox without --host, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNodeJoinProxmoxMissingPassword(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "join", "--type", "proxmox", "--host", "10.0.0.99"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for proxmox without password, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNodeListEmpty(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "list"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node list: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNodeListAfterJoin(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "lister", "--addr", "10.0.0.7:8443"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node join: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "list"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node list: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "lister") {
|
|
t.Errorf("node list missing joined node: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestNodeListJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
rootCmd.SetArgs([]string{"node", "join", "--name", "jsonlister", "--addr", "10.0.0.8:8443"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node join: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "list", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node list --json: %v", err)
|
|
}
|
|
var nodes []map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &nodes); err != nil {
|
|
t.Fatalf("unmarshal node list json: %v\n%s", err, buf.String())
|
|
}
|
|
found := false
|
|
for _, n := range nodes {
|
|
if n["name"] == "jsonlister" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("node list --json missing jsonlister: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestNodeLeave(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
nodeID := seedNode(t, "leaver", "10.0.0.9:8443")
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "leave", nodeID})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node leave: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "Node left") {
|
|
t.Errorf("node leave output unexpected: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestNodeLeaveJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
nodeID := seedNode(t, "jsonleaver", "10.0.0.10:8443")
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "leave", nodeID, "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("node leave --json: %v", err)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
|
t.Fatalf("unmarshal node leave json: %v\n%s", err, buf.String())
|
|
}
|
|
if result["state"] != "left" {
|
|
t.Errorf("node leave --json state = %v, want left", result["state"])
|
|
}
|
|
}
|
|
|
|
func TestNodeLeaveMissingID(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"node", "leave"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for node leave without id, got nil")
|
|
}
|
|
}
|
|
|
|
func seedNode(t *testing.T, name, addr string) string {
|
|
t.Helper()
|
|
db, err := store.Open(certpaths.DBPath())
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
repo := store.NewNodeRepo(db)
|
|
ctx := context.Background()
|
|
n := &model.Node{
|
|
ID: "node-" + name,
|
|
Name: name,
|
|
Address: addr,
|
|
State: model.NodeStateReady,
|
|
JoinedAt: time.Now().UTC(),
|
|
LastSeen: time.Now().UTC(),
|
|
}
|
|
if err := repo.Insert(ctx, n); err != nil {
|
|
t.Fatalf("insert node: %v", err)
|
|
}
|
|
return n.ID
|
|
}
|
|
|
|
func padHex(n int) string {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = 'a'
|
|
}
|
|
return string(b)
|
|
}
|