package proxmox import ( "bytes" "context" "errors" "log/slog" "os" "path/filepath" "strings" "testing" "golang.org/x/crypto/ssh" ) func TestSudoersContent(t *testing.T) { content := sudoersContent("orca") if !strings.Contains(content, "NOPASSWD: NOEXEC: /usr/bin/pct") { t.Error("missing NOEXEC on pct (AD-020)") } if !strings.Contains(content, "NOPASSWD: NOEXEC: /usr/bin/qm") { t.Error("missing NOEXEC on qm (AD-020)") } if !strings.Contains(content, "NOPASSWD: /usr/bin/apt-get") { t.Error("missing NOPASSWD on apt-get") } if !strings.Contains(content, "NOPASSWD: /usr/bin/dpkg") { t.Error("missing NOPASSWD on dpkg") } if strings.Contains(content, "NOEXEC: /usr/bin/apt-get") { t.Error("apt-get must NOT have NOEXEC (breaks maintainer scripts)") } if strings.Contains(content, "NOEXEC: /usr/bin/dpkg") { t.Error("dpkg must NOT have NOEXEC (breaks maintainer scripts)") } for _, line := range strings.Split(content, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "#") || trimmed == "" { continue } if strings.Contains(trimmed, "pvesh") { t.Errorf("pvesh must be EXCLUDED from sudoers command lines (AD-020): %s", trimmed) } } if !strings.HasPrefix(content, "# /etc/sudoers.d/orca") { t.Error("missing managed-by-orca header") } if !strings.Contains(content, "orca ALL=(root)") { t.Error("missing orca user in sudoers") } } func TestSudoersContent_CustomUser(t *testing.T) { content := sudoersContent("custom-orca") if !strings.Contains(content, "custom-orca ALL=(root)") { t.Error("missing custom-orca user in sudoers") } } func TestOrcaOperatorPrivileges(t *testing.T) { privs := strings.Fields(OrcaOperatorPrivileges) expected := map[string]bool{ "VM.Audit": true, "Datastore.AllocateSpace": true, "SDN.Use": true, } if len(privs) != 3 { t.Errorf("expected 3 privileges, got %d: %v", len(privs), privs) } for _, p := range privs { if !expected[p] { t.Errorf("unexpected privilege %q", p) } } } func TestBootstrapProxmox_Validation(t *testing.T) { ctx := context.Background() _, err := BootstrapProxmox(ctx, Options{Password: "pw"}) if err == nil || !strings.Contains(err.Error(), "host is required") { t.Errorf("expected host-required error, got %v", err) } _, err = BootstrapProxmox(ctx, Options{Host: "10.0.0.1"}) if err == nil || !strings.Contains(err.Error(), "password is required") { t.Errorf("expected password-required error, got %v", err) } } func TestDefaultOptions(t *testing.T) { if DefaultProxmoxUser != "orca" { t.Errorf("DefaultProxmoxUser = %q, want orca", DefaultProxmoxUser) } if DefaultProxmoxRole != "OrcaOperator" { t.Errorf("DefaultProxmoxRole = %q, want OrcaOperator", DefaultProxmoxRole) } if DefaultSSHPort != 22 { t.Errorf("DefaultSSHPort = %d, want 22", DefaultSSHPort) } } type mockSSHDialer struct { client *ssh.Client err error calls int lastAddr string lastCfg *ssh.ClientConfig } func (m *mockSSHDialer) DialContext(ctx context.Context, network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { m.calls++ m.lastAddr = addr m.lastCfg = config if m.err != nil { return nil, m.err } return m.client, nil } func setupORCAHome(t *testing.T) string { t.Helper() dir := t.TempDir() t.Setenv("ORCA_HOME", dir) knownHosts := filepath.Join(dir, "known_hosts") if err := os.WriteFile(knownHosts, []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } return dir } func TestBootstrapProxmox_SSHAuthFailure(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("ssh: handshake failed: ssh: unable to authenticate")} setupORCAHome(t) _, err := BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", }) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "ssh") { t.Errorf("error should mention ssh, got: %v", err) } if !strings.Contains(err.Error(), "ssh dial") { t.Errorf("error should mention ssh dial, got: %v", err) } } func TestBootstrapProxmox_SSHDialCalledWithCorrectAddr(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() dialer := &mockSSHDialer{err: errors.New("connection refused")} sshDialer = dialer setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.42", Password: "pw", SSHPort: 2222, }) if dialer.calls != 1 { t.Errorf("dialer calls = %d, want 1", dialer.calls) } if dialer.lastAddr != "10.0.0.42:2222" { t.Errorf("dial addr = %q, want 10.0.0.42:2222", dialer.lastAddr) } } func TestBootstrapProxmox_DefaultSSHPort(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() dialer := &mockSSHDialer{err: errors.New("connection refused")} sshDialer = dialer setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.99", Password: "pw", }) if dialer.lastAddr != "10.0.0.99:22" { t.Errorf("dial addr = %q, want 10.0.0.99:22 (default port)", dialer.lastAddr) } } func TestBootstrapProxmox_CustomSSHUser(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() dialer := &mockSSHDialer{err: errors.New("connection refused")} sshDialer = dialer setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", SSHUser: "custom-admin", }) if dialer.calls != 1 { t.Errorf("dialer calls = %d, want 1", dialer.calls) } if dialer.lastCfg == nil || dialer.lastCfg.User != "custom-admin" { t.Errorf("ssh user not propagated, got %+v", dialer.lastCfg) } } func TestBootstrapProxmox_SSHKeyGenerated(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} dir := setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", }) keyPath := filepath.Join(dir, "orca_ssh_key") pubPath := filepath.Join(dir, "orca_ssh_key.pub") if _, err := os.Stat(keyPath); err != nil { t.Errorf("SSH key not generated at %s: %v", keyPath, err) } if _, err := os.Stat(pubPath); err != nil { t.Errorf("SSH pub not generated at %s: %v", pubPath, err) } } func TestBootstrapProxmox_KnownHostsFileCreated(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} dir := setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", }) knownHosts := filepath.Join(dir, "known_hosts") if _, err := os.Stat(knownHosts); err != nil { t.Errorf("known_hosts not created at %s: %v", knownHosts, err) } } func TestBootstrapProxmox_NilLogger(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} setupORCAHome(t) defer func() { if r := recover(); r != nil { t.Fatalf("nil logger panicked: %v", r) } }() _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", Logger: nil, }) } func TestBootstrapProxmox_CustomLogger(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} setupORCAHome(t) var buf bytes.Buffer log := slog.New(slog.NewTextHandler(&buf, nil)) defer func() { if r := recover(); r != nil { t.Fatalf("custom logger panicked: %v", r) } }() _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", Logger: log, }) _ = buf.String() } func TestBootstrapProxmox_ContextCancelled(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} setupORCAHome(t) ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := BootstrapProxmox(ctx, Options{ Host: "10.0.0.1", Password: "pw", }) if err == nil { t.Fatal("expected error with cancelled context") } } func TestDeployPubKey_EmptyPubLine(t *testing.T) { err := deployPubKey(nil, "orca", "") if err == nil { t.Error("expected error for empty pub line") } if !strings.Contains(err.Error(), "empty pub line") { t.Errorf("error should mention empty pub line, got: %v", err) } } func TestDeployPubKey_WhitespaceOnlyPubLine(t *testing.T) { err := deployPubKey(nil, "orca", " \n \t ") if err == nil { t.Error("expected error for whitespace-only pub line") } }