Has anyone had any success or have any ideas on what would be the best way to mock entry (for testing purposes) to a term.ReadPassword(int(os.Stdin.Fd()))
call in the golang.org/x/crypto/ssh/terminal
package?
I have tried creating a temp file (vs os.Stdin
) and writing string values like testing
or testing
to the temp file but I get the error inappropriate ioctl for device
. I'm guessing it is something TTY related or a specific format that is missing(?) but I really am not sure.
Help appreciated.
If you are stubbing this test by creating a fake file that os.Stdin
is referencing, your tests will become tremendously OS specific when you try to handle ReadPassword()
. This is because under the hood Go is compiling separate syscalls depending on the OS. ReadPassword()
is implemented here, but the syscalls based on architecture and OS are in this directory. As you can see there are many. I cannot think of a good way to stub this test in the way you are specifying.
With the limited understanding of your problem the solution I would propose would be to inject a simple interface along the lines of:
type PasswordReader interface {
ReadPassword(fd int) ([]byte, error)
}
func (pr PasswordReader) ReadPassword(fd int) ([]byte, error) {
return terminal.ReadPassword(fd)
}
This way you can pass in a fake object to your tests, and stub the response to ReadPassword
. I know this feels like writing your code for your tests, but you can reframe this thought as terminal
is an outside dependency (I/O) that should be injected! So now your tests are not only ensuring your code works, but actually helping you make good design decisions.
Corbin's example prompted me to look into interface mocking and prompted me to write up a basic example:
// getter.go
package cmd
import (
"errors"
"syscall"
"golang.org/x/crypto/ssh/terminal"
)
type PasswordReader interface {
ReadPassword() (string, error)
}
type StdInPasswordReader struct {
}
func (pr StdInPasswordReader) ReadPassword() (string, error) {
pwd, error := terminal.ReadPassword(int(syscall.Stdin))
return string(pwd), error
}
func readPassword(pr PasswordReader) (string, error) {
pwd, err := pr.ReadPassword()
if err != nil {
return "", err
}
if len(pwd) == 0 {
return "", errors.New("empty password provided")
}
return pwd, nil
}
func Run(pr PasswordReader) (string, error) {
pwd, err := readPassword(pr)
if err != nil {
return "", err
}
return string(pwd), nil
}
In the test, we can mock errors and simulating no stdin
input.
// getter_test.go
package cmd_test
import (
"errors"
"testing"
"github.com/petems/passwordgetter/cmd"
"github.com/stretchr/testify/assert"
)
type stubPasswordReader struct {
Password string
ReturnError bool
}
func (pr stubPasswordReader) ReadPassword() (string, error) {
if pr.ReturnError {
return "", errors.New("stubbed error")
}
return pr.Password, nil
}
func TestRunReturnsErrorWhenReadPasswordFails(t *testing.T) {
pr := stubPasswordReader{ReturnError: true}
result, err := cmd.Run(pr)
assert.Error(t, err)
assert.Equal(t, errors.New("stubbed error"), err)
assert.Equal(t, "", result)
}
func TestRunReturnsPasswordInput(t *testing.T) {
pr := stubPasswordReader{Password: "password"}
result, err := cmd.Run(pr)
assert.NoError(t, err)
assert.Equal(t, "password", result)
}
There are also tools like gomock, testify and counterfeiter that basically do all the heavy lifting for you, and you can add in generator steps into the code:
//go:generate mockgen -destination=../mocks/mock_getter.go -package=mocks github.com/petems/passwordgetter/cmd PasswordReader
You then include the mock that gets generated in your test:
package cmd_test
import (
"testing"
"errors"
"github.com/golang/mock/gomock"
"github.com/petems/passwordgetter/mocks"
"github.com/petems/passwordgetter/cmd"
"github.com/stretchr/testify/assert"
)
func TestRunReturnsErrorWhenEmptyString(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockPasswordReader := mocks.NewMockPasswordReader(mockCtrl)
mockPasswordReader.EXPECT().ReadPassword().Return("", nil).Times(1)
result, err := cmd.Run(mockPasswordReader)
assert.Error(t, err)
assert.Equal(t, errors.New("empty password provided"), err)
assert.Equal(t, "", result)
}