Skip to content

Commit

Permalink
test(context): add unit tests (#755)
Browse files Browse the repository at this point in the history
This PR adds tests to the `context` subcommands using the new
configuration system implemented in #736
  • Loading branch information
phm07 authored Jun 6, 2024
1 parent 9b34d26 commit 0db5046
Show file tree
Hide file tree
Showing 15 changed files with 639 additions and 26 deletions.
2 changes: 1 addition & 1 deletion internal/cmd/context/active.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/hetznercloud/cli/internal/state"
)

func newActiveCommand(s state.State) *cobra.Command {
func NewActiveCommand(s state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "active",
Short: "Show active context",
Expand Down
92 changes: 92 additions & 0 deletions internal/cmd/context/active_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package context_test

import (
"os"
"testing"

"github.com/stretchr/testify/assert"

"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/testutil"
)

func TestActive(t *testing.T) {

testConfig := `
active_context = "my-context"
[[contexts]]
name = "my-context"
token = "super secret token"
`

type testCase struct {
name string
args []string
config string
err string
expOut string
expErr string
preRun func()
postRun func()
}

testCases := []testCase{
{
name: "no arguments",
args: []string{},
config: testConfig,
expOut: "my-context\n",
},
{
name: "no config",
args: []string{},
},
{
name: "from env",
args: []string{},
config: testConfig,
preRun: func() {
_ = os.Setenv("HCLOUD_CONTEXT", "abcdef")
},
postRun: func() {
_ = os.Unsetenv("HCLOUD_CONTEXT")
},
// 'abcdef' does not exist, so there is nothing printed to stdout.
// The warning 'active context "abcdef" not found' should be printed to stderr during config loading, which
// is before stderr is captured.
},
{
name: "invalid config",
args: []string{},
config: `active_context = "invalid-context-name"`,
// if there is no context with the name of the active_context, there should be no output. See above
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {

if tt.preRun != nil {
tt.preRun()
}
if tt.postRun != nil {
defer tt.postRun()
}

fx := testutil.NewFixtureWithConfigFile(t, []byte(tt.config))
defer fx.Finish()

cmd := context.NewActiveCommand(fx.State())
out, errOut, err := fx.Run(cmd, tt.args)

if tt.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.err)
}
assert.Equal(t, tt.expErr, errOut)
assert.Equal(t, tt.expOut, out)
})
}
}
10 changes: 5 additions & 5 deletions internal/cmd/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ func NewCommand(s state.State) *cobra.Command {
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
newCreateCommand(s),
newActiveCommand(s),
newUseCommand(s),
newDeleteCommand(s),
newListCommand(s),
NewCreateCommand(s),
NewActiveCommand(s),
NewUseCommand(s),
NewDeleteCommand(s),
NewListCommand(s),
)
return cmd
}
11 changes: 5 additions & 6 deletions internal/cmd/context/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,28 @@ import (
"syscall"

"github.com/spf13/cobra"
"golang.org/x/term"

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/cli/internal/ui"
)

func newCreateCommand(s state.State) *cobra.Command {
func NewCreateCommand(s state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "create <name>",
Short: "Create a new context",
Args: util.Validate,
TraverseChildren: true,
DisableFlagsInUseLine: true,
SilenceUsage: true,
RunE: state.Wrap(s, runCreate),
}
return cmd
}

func runCreate(s state.State, cmd *cobra.Command, args []string) error {
cfg := s.Config()
if !ui.StdoutIsTerminal() {
if !s.Terminal().StdoutIsTerminal() {
return errors.New("context create is an interactive command")
}

Expand Down Expand Up @@ -63,8 +62,8 @@ func runCreate(s state.State, cmd *cobra.Command, args []string) error {
for {
cmd.Printf("Token: ")
// Conversion needed for compilation on Windows
// vvv
btoken, err := term.ReadPassword(int(syscall.Stdin))
// vvv
btoken, err := s.Terminal().ReadPassword(int(syscall.Stdin))
cmd.Print("\n")
if err != nil {
return err
Expand Down
121 changes: 121 additions & 0 deletions internal/cmd/context/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package context_test

import (
"io"
"syscall"
"testing"

"github.com/stretchr/testify/assert"

"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/testutil"
)

func TestCreate(t *testing.T) {

testConfig := `
active_context = "my-context"
[[contexts]]
name = "my-context"
token = "super secret token"
`

type testCase struct {
name string
args []string
config string
isTerm bool
token string
err string
expErr string
expOut string
}

testCases := []testCase{
{
name: "new context",
args: []string{"new-context"},
isTerm: true,
config: testConfig,
token: "q4acIB6pq2CwsPqF+dNR2B6NTrv4yxmsspvDC1a02OqfMQeCz7nOk4A3pcJha8ix",
expOut: `Token:
active_context = "new-context"
[[contexts]]
name = "my-context"
token = "super secret token"
[[contexts]]
name = "new-context"
token = "q4acIB6pq2CwsPqF+dNR2B6NTrv4yxmsspvDC1a02OqfMQeCz7nOk4A3pcJha8ix"
Context new-context created and activated
`,
},
{
name: "not terminal",
args: []string{"new-context"},
isTerm: false,
config: testConfig,
err: "context create is an interactive command",
expErr: "Error: context create is an interactive command\n",
},
{
name: "existing context",
args: []string{"my-context"},
isTerm: true,
config: testConfig,
token: "q4acIB6pq2CwsPqF+dNR2B6NTrv4yxmsspvDC1a02OqfMQeCz7nOk4A3pcJha8ix",
err: "name already used",
expErr: "Error: name already used\n",
},
{
name: "invalid name",
args: []string{""},
isTerm: true,
config: testConfig,
err: "invalid name",
expErr: "Error: invalid name\n",
},
{
name: "token too short",
args: []string{"new-context"},
isTerm: true,
config: testConfig,
token: "abc",
err: "EOF",
expErr: "Error: EOF\n",
expOut: "Token: \nEntered token is invalid (must be exactly 64 characters long)\nToken: \n",
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
fx := testutil.NewFixtureWithConfigFile(t, []byte(tt.config))
defer fx.Finish()

fx.Terminal.EXPECT().StdoutIsTerminal().Return(tt.isTerm)

isFirstCall := true
fx.Terminal.EXPECT().ReadPassword(int(syscall.Stdin)).DoAndReturn(func(_ int) ([]byte, error) {
if isFirstCall {
isFirstCall = false
return []byte(tt.token), nil
}
// return EOF after first call to prevent infinite loop
return nil, io.EOF
}).AnyTimes()

cmd := context.NewCreateCommand(fx.State())
out, errOut, err := fx.Run(cmd, tt.args)

if tt.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.err)
}
assert.Equal(t, tt.expErr, errOut)
assert.Equal(t, tt.expOut, out)
})
}
}
3 changes: 2 additions & 1 deletion internal/cmd/context/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import (
"github.com/hetznercloud/cli/internal/state/config"
)

func newDeleteCommand(s state.State) *cobra.Command {
func NewDeleteCommand(s state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "delete <context>",
Short: "Delete a context",
Args: util.Validate,
ValidArgsFunction: cmpl.SuggestArgs(cmpl.SuggestCandidates(config.ContextNames(s.Config())...)),
TraverseChildren: true,
DisableFlagsInUseLine: true,
SilenceUsage: true,
RunE: state.Wrap(s, runDelete),
}
return cmd
Expand Down
85 changes: 85 additions & 0 deletions internal/cmd/context/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package context_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/testutil"
)

func TestDelete(t *testing.T) {

testConfig := `
active_context = "my-context"
[[contexts]]
name = "my-context"
token = "super secret token"
[[contexts]]
name = "my-other-context"
token = "super secret token"
`

type testCase struct {
name string
args []string
config string
err string
expErr string
expOut string
}

testCases := []testCase{
{
name: "delete active context",
args: []string{"my-context"},
config: testConfig,
expErr: "Warning: You are deleting the currently active context. Please select a new active context.\n",
expOut: `active_context = ""
[[contexts]]
name = "my-other-context"
token = "super secret token"
`,
},
{
name: "delete inactive context",
args: []string{"my-other-context"},
config: testConfig,
expOut: `active_context = "my-context"
[[contexts]]
name = "my-context"
token = "super secret token"
`,
},
{
name: "delete non-existing context",
args: []string{"non-existing-context"},
config: testConfig,
err: "context not found: non-existing-context",
expErr: "Error: context not found: non-existing-context\n",
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
fx := testutil.NewFixtureWithConfigFile(t, []byte(tt.config))
defer fx.Finish()

cmd := context.NewDeleteCommand(fx.State())
out, errOut, err := fx.Run(cmd, tt.args)

if tt.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.err)
}
assert.Equal(t, tt.expErr, errOut)
assert.Equal(t, tt.expOut, out)
})
}
}
Loading

0 comments on commit 0db5046

Please sign in to comment.