* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package clusterctrl
import (
"errors"
"io"
"os/exec"
"strings"
"testing"
"cli/utils"
"github.com/agiledragon/gomonkey/v2"
. "github.com/smartystreets/goconvey/convey"
)
func TestSSHRemoteExecutor(t *testing.T) {
dummyMockExecutor := &mockExecutor{}
Convey("Test SSHRemoteExecutor", t, func() {
Convey("prepareCommand", func() {
Convey("Given a valid SSH command template", func() {
template := "ssh -o ConnectTimeout=5 " + utils.PlaceholderTarget + " " + utils.PlaceholderCommand
se := &SSHRemoteExecutor{sshCmdTemplate: template}
Convey("When prepareCommand is called with host and command", func() {
cmd := se.prepareCommand("test-host", "echo hello")
Convey("Then it should construct the correct command", func() {
So(len(cmd.Args), ShouldEqual, 6)
So(cmd.Args[0], ShouldEqual, "ssh")
So(cmd.Args[1], ShouldEqual, "-o")
So(cmd.Args[2], ShouldEqual, "ConnectTimeout=5")
So(cmd.Args[3], ShouldEqual, "test-host")
So(cmd.Args[4], ShouldEqual, "echo")
So(cmd.Args[5], ShouldEqual, "hello")
})
})
})
})
Convey("RunCommandWithPipe", func() {
Convey("Given a successful SSH command execution", func() {
template := "ssh " + utils.PlaceholderTarget + " " + utils.PlaceholderCommand
se := NewSSHRemoteExecutor(template).(*SSHRemoteExecutor)
output := "hello from remote\n"
mockPipe := &mockReadCloser{Reader: strings.NewReader(output), closer: dummyMockExecutor}
cmd := exec.Command("ssh", "test-host", "echo hello")
patches := gomonkey.NewPatches()
patches.ApplyMethod(cmd, "StdoutPipe", func(*exec.Cmd) (io.ReadCloser, error) {
return mockPipe, nil
})
patches.ApplyMethod(cmd, "Start", func(*exec.Cmd) error {
return nil
})
patches.ApplyMethod(cmd, "Wait", func(*exec.Cmd) error {
return nil
})
patches.ApplyFunc(exec.Command, func(name string, args ...string) *exec.Cmd {
return cmd
})
defer patches.Reset()
Convey("When RunCommandWithPipe is called", func() {
reader, waitFn, err := se.RunCommandWithPipe("test-host", "echo hello", []string{})
Convey("Then it should return a valid reader and wait function without error", func() {
So(err, ShouldBeNil)
So(reader, ShouldNotBeNil)
So(waitFn, ShouldNotBeNil)
buf := make([]byte, 1024)
n, readErr := reader.Read(buf)
So(readErr, ShouldBeNil)
So(string(buf[:n]), ShouldEqual, "hello from remote\n")
waitErr := waitFn()
So(waitErr, ShouldBeNil)
closeErr := reader.Close()
So(closeErr, ShouldBeNil)
})
})
})
Convey("Given an SSH command with stdout pipe error", func() {
template := "ssh " + utils.PlaceholderTarget + " " + utils.PlaceholderCommand
se := NewSSHRemoteExecutor(template).(*SSHRemoteExecutor)
cmd := exec.Command("ssh", "test-host", "echo hello")
patches := gomonkey.NewPatches()
patches.ApplyMethod(cmd, "StdoutPipe", func(*exec.Cmd) (io.ReadCloser, error) {
return nil, errors.New("stdout pipe not available")
})
patches.ApplyFunc(exec.Command, func(name string, args ...string) *exec.Cmd {
return cmd
})
defer patches.Reset()
Convey("When RunCommandWithPipe is called", func() {
reader, waitFn, err := se.RunCommandWithPipe("test-host", "echo hello", []string{})
Convey("Then it should return an error", func() {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, "failed to get stdout pipe")
So(reader, ShouldBeNil)
So(waitFn, ShouldBeNil)
})
})
})
Convey("Given an SSH command that fails to start", func() {
template := "ssh " + utils.PlaceholderTarget + " " + utils.PlaceholderCommand
se := NewSSHRemoteExecutor(template).(*SSHRemoteExecutor)
mockPipe := &mockReadCloser{Reader: strings.NewReader(""), closer: dummyMockExecutor}
cmd := exec.Command("ssh", "test-host", "echo hello")
patches := gomonkey.NewPatches()
patches.ApplyMethod(cmd, "StdoutPipe", func(*exec.Cmd) (io.ReadCloser, error) {
return mockPipe, nil
})
patches.ApplyMethod(cmd, "Start", func(*exec.Cmd) error {
return errors.New("ssh connection failed")
})
patches.ApplyFunc(exec.Command, func(name string, args ...string) *exec.Cmd {
return cmd
})
defer patches.Reset()
Convey("When RunCommandWithPipe is called", func() {
reader, waitFn, err := se.RunCommandWithPipe("test-host", "echo hello", []string{})
Convey("Then it should return an error and close the pipe", func() {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, "failed to start command")
So(reader, ShouldBeNil)
So(waitFn, ShouldBeNil)
So(mockPipe.closer.closed, ShouldBeTrue)
})
})
})
Convey("Given an SSH command that fails on wait", func() {
template := "ssh " + utils.PlaceholderTarget + " " + utils.PlaceholderCommand
se := NewSSHRemoteExecutor(template).(*SSHRemoteExecutor)
output := "partial output\n"
mockPipe := &mockReadCloser{Reader: strings.NewReader(output), closer: dummyMockExecutor}
cmd := exec.Command("ssh", "test-host", "fail command")
patches := gomonkey.NewPatches()
patches.ApplyMethod(cmd, "StdoutPipe", func(*exec.Cmd) (io.ReadCloser, error) {
return mockPipe, nil
})
patches.ApplyMethod(cmd, "Start", func(*exec.Cmd) error {
return nil
})
patches.ApplyMethod(cmd, "Wait", func(*exec.Cmd) error {
return errors.New("command exited with error")
})
patches.ApplyFunc(exec.Command, func(name string, args ...string) *exec.Cmd {
return cmd
})
defer patches.Reset()
Convey("When RunCommandWithPipe is called", func() {
reader, waitFn, err := se.RunCommandWithPipe("test-host", "fail command", []string{})
Convey("Then it should return a reader and a wait function with error", func() {
So(err, ShouldBeNil)
So(reader, ShouldNotBeNil)
So(waitFn, ShouldNotBeNil)
buf := make([]byte, 1024)
n, readErr := reader.Read(buf)
So(readErr, ShouldBeNil)
So(string(buf[:n]), ShouldEqual, "partial output\n")
waitErr := waitFn()
So(waitErr, ShouldResemble, errors.New("command exited with error"))
})
})
})
})
})
}